diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index aab82b89..67de9fc6 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
- python-version: 3.8
+ python-version: "3.10"
- name: install dependencies
run: |
python -m pip install --upgrade pip
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c8bf50d1..dfa225f2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,16 +9,16 @@ jobs:
fail-fast: false
matrix:
include:
- - python: 3.8
+ - python: "3.10"
toxenv: flake8
os: ubuntu-latest
- - python: 3.8
+ - python: "3.10"
toxenv: mypy
os: ubuntu-latest
- - python: 3.8
+ - python: "3.10"
toxenv: pylint
os: ubuntu-latest
- - python: 3.8
+ - python: "3.10"
toxenv: black
os: ubuntu-latest
@@ -31,15 +31,21 @@ jobs:
- python: "3.10"
toxenv: py310
os: ubuntu-latest
+ - python: "3.11"
+ toxenv: py311
+ os: ubuntu-latest
- python: pypy-3.8
toxenv: pypy38
os: ubuntu-latest
+ - python: pypy-3.9
+ toxenv: pypy39
+ os: ubuntu-latest
- - python: 3.8
- toxenv: py38
+ - python: "3.10"
+ toxenv: py310
os: macos-latest
- - python: 3.8
- toxenv: py38
+ - python: "3.10"
+ toxenv: py310
os: windows-latest
runs-on: ${{ matrix.os }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 765ae975..fe30c3b4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,5 @@
repos:
- repo: https://github.com/psf/black
- rev: 21.10b0
+ rev: 22.10.0
hooks:
- id: black
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fe25859..8a6bcbf9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,95 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
-## [Unreleased]
+
+## Unreleased
+
+## [4.0.0 - 2024-03-04]
+### Added
+- added `TileScopePack.requirement_and_row_and_col_placements`
+- `AssumptionAndPointJumpingFactory` which adds rules where requirements and/or
+ assumptions are swapped around a fusable row or column.
+- `PointJumpingFactory` which adds rules where requirements and assumptions can be
+swapped around a fusable row or column.
+- `MonotoneSlidingFactory` that creates rules that swaps neighbouring cells if they
+ are 'monotone' fusable, i.e., they are a generalized fusion with a monotone local
+ extra obstruction.
+- `DeflationFactory` which adds rules where cells can be deflated into increasing or
+ decreasing cells as obstructions can't occur across the sum/skew components in that
+ cell.
+- `CellReductionFactory` which changes a cell to monotone if at most one point of
+ any crossing gp touches that cell.
+- `PositiveCorroborationFactory` that inserts into cells which if positive makes
+ another cell empty. Also, the `PointCorroborationFactory`, which does this for
+ point or empty cells which is added to most packs.
+- `TargetedCellInsertionFactory` which inserts factors of gridded perms if it can
+ lead to factoring out a verified sub tiling.
+- `BasisPatternInsertionFactory` which inserts permutations which are contained in
+ every pattern in the basis
+- `ComponentVerificationStrategy` which is added to component fusion packs.
+- `ComponentToPointAssumptionStrategy` that changes component assumptions to point
+ assumptions. These strategies are yielded in `RearrangeAssumptionFactory`.
+- `StrategyPack.kitchen_sinkify` to add many experimental strategies to the pack
+- `SubobstructionInsertionFactory` that inserts subobstructions and the pack
+ `TileScopePack.subobstruction_placements` which uses it.
+- `FactorWithInterleavingStrategy.backward_map` so you can now generate permutation
+ from specifications using interleaving factors.
+- `DummyStrategy` that gives a quick template for making strategies.
+- `PointingStrategy`, `AssumptionPointingFactory` and `RequirementPointingFactory`
+ that place points directionless in non-point cells. This are a non-productive
+ strategy so should be used with `RuleDBForest`.
+- `UnfusionFactory` that unfuses either all the rows or columns. Also non-productive.
+- `FusableRowAndColumnPlacementFactory` places fusable rows and columns.
+- `TrackedClassDB` used by `TrackedSearcher`
+- counting for `GeneralizedSlidingStrategy` of rows (i.e., `rotate=True`)
+
+### Fixed
+- `Factor` was not factoring correctly with respect to component assumptions.
+- `ComponentAssumption` are flipped when taking symmetries
+- `Tiling.get_minimum_value` fixed for component assumptions
+- `RearrangeAssumptionFactory` will ignore component assumptions
+- `GriddedPermReduction.minimal_reqs` was removing requirements if they
+ were duplicates.
+- `RequirementPlacement` algorithm didn't minimise obstructions correctly when
+ placing size 2 or higher gridded perms.
+- added missing condition in `MonotoneSlidingFactory` for consecutive
+ values. Previous rules failing this condition will now raise
+ `StrategyDoesNotApply` if it fails this condition.
+- `LocalVerificationStrategy` needs to be a `BasisAwareVerificationStrategy`
+- `PointJumping` maps component assumption to component assumptions.
+- `Tiling.all_symmetries` had a premature break statement that was removed
+- `shift_from_spec` method would previously fail if any tiling had two or
+ more interleaving cells.
+
+### Changed
+- `TileScopePack.make_tracked` will add the appropriate tracking methods for
+ interleaving factors and make strategies tracked if it can be.
+- The `GriddedPermReduction` limits the size of obstructions it tries to infer in
+ the `minimal_obs` method to the size of the largest obstruction already on the
+ tiling.
+- The `SymmetriesFactory` takes a basis and will not return any symmetries where
+ any of the patterns of the obstruction are not subpatterns of some basis element.
+ If no basis is given, all symmetries are returned.
+- `RequirementPlacement` adds empty cells when placing a point cell. This saves
+ some inferral in partial placements.
+- Don't reinitialise in the `Tiling.from_dict` method.
+- `GuidedSearcher` expands every symmetry
+- `TileScopePack.pattern_placements` factors as an initial strategy.
+- `is_component` method of assumptions updated to consider cell decomposition
+- `AddAssumptionsStrategy.is_reverible` is now True when the assumption covers the
+ whole tiling.
+- The default behavior for `RequirementInsertion` is to allow insertion of factorable
+ requirements
+- `OneByOneVerificationStrategy` will look up permpal.com to find the generating
+ functions and min polys, and also use permpal specs for counting, sampling and
+ generating objects.
+- The `kitchen_sinkify` function on `TileScopePack` now takes a level between 1 and 5
+ as input, which is used to determine how crazy the added strategies should be.
+
+### Removed
+- `AddInterleavingAssumptionsFactory`. The factor strategy now adds the relevant
+ assumptions where necessary directly, lowering the number of CVs needed.
+
## [3.1.0] - 2022-01-17
### Added
diff --git a/README.rst b/README.rst
index c16f0463..fa152180 100644
--- a/README.rst
+++ b/README.rst
@@ -573,7 +573,7 @@ You can make any pack use the fusion strategy by using the method
>>> print(pack)
Looking for recursive combinatorial specification with the strategies:
Inferral: row and column separation, obstruction transitivity
- Initial: rearrange assumptions, add assumptions, factor, tracked fusion
+ Initial: rearrange assumptions, add assumptions, factor, point corroboration, tracked fusion
Verification: verify atoms, insertion encoding verified, one by one verification, locally factorable verification
Set 1: row placement
diff --git a/mypy.ini b/mypy.ini
index 34a4b00a..31d8ccf7 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,4 +1,5 @@
[mypy]
+check_untyped_defs = True
warn_return_any = True
warn_unused_configs = True
warn_no_return = False
diff --git a/pylintrc b/pylintrc
index 393d3c14..7ef81862 100644
--- a/pylintrc
+++ b/pylintrc
@@ -28,7 +28,7 @@ limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
-load-plugins=
+load-plugins=pylint.extensions.no_self_use
# Pickle collected data for later comparisons.
persistent=yes
@@ -63,6 +63,7 @@ confidence=
disable=missing-class-docstring,
too-few-public-methods,
unsubscriptable-object, # Pylint does not support Genric class see https://github.com/PyCQA/pylint/issues/3520
+ wrong-import-order, # isort is taking care of import order for us
cyclic-import,
no-member,
@@ -75,19 +76,8 @@ disable=missing-class-docstring,
inconsistent-return-statements,
invalid-name,
unused-argument,
- bad-continuation,
missing-function-docstring,
missing-module-docstring,
- print-statement,
- parameter-unpacking,
- unpacking-in-except,
- old-raise-syntax,
- backtick,
- long-suffix,
- old-ne-operator,
- old-octal-literal,
- import-star-module-level,
- non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
@@ -95,68 +85,7 @@ disable=missing-class-docstring,
suppressed-message,
useless-suppression,
deprecated-pragma,
- use-symbolic-message-instead,
- apply-builtin,
- basestring-builtin,
- buffer-builtin,
- cmp-builtin,
- coerce-builtin,
- execfile-builtin,
- file-builtin,
- long-builtin,
- raw_input-builtin,
- reduce-builtin,
- standarderror-builtin,
- unicode-builtin,
- xrange-builtin,
- coerce-method,
- delslice-method,
- getslice-method,
- setslice-method,
- no-absolute-import,
- old-division,
- dict-iter-method,
- dict-view-method,
- next-method-called,
- metaclass-assignment,
- indexing-exception,
- raising-string,
- reload-builtin,
- oct-method,
- hex-method,
- nonzero-method,
- cmp-method,
- input-builtin,
- round-builtin,
- intern-builtin,
- unichr-builtin,
- map-builtin-not-iterating,
- zip-builtin-not-iterating,
- range-builtin-not-iterating,
- filter-builtin-not-iterating,
- using-cmp-argument,
- eq-without-hash,
- div-method,
- idiv-method,
- rdiv-method,
- exception-message-attribute,
- invalid-str-codec,
- sys-max-int,
- bad-python3-import,
- deprecated-string-function,
- deprecated-str-translate-call,
- deprecated-itertools-function,
- deprecated-types-field,
- next-method-defined,
- dict-items-not-iterating,
- dict-keys-not-iterating,
- dict-values-not-iterating,
- deprecated-operator-function,
- deprecated-urllib-function,
- xreadlines-attribute,
- deprecated-sys-function,
- exception-escape,
- comprehension-escape
+ use-symbolic-message-instead
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@@ -223,13 +152,6 @@ max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
- dict-separator
-
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
diff --git a/setup.py b/setup.py
index eba8b042..fe3f7709 100755
--- a/setup.py
+++ b/setup.py
@@ -34,10 +34,10 @@ def get_version(rel_path):
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
long_description=read("README.rst"),
install_requires=[
- "comb-spec-searcher==4.1.0",
- "permuta==2.2.0",
+ "comb-spec-searcher==4.2.0",
+ "permuta==2.3.0",
],
- python_requires=">=3.7",
+ python_requires=">=3.8",
include_package_data=True,
classifiers=[
"Development Status :: 5 - Production/Stable",
diff --git a/tests/algorithms/test_enumeration.py b/tests/algorithms/test_enumeration.py
index 4a17469c..659c462e 100644
--- a/tests/algorithms/test_enumeration.py
+++ b/tests/algorithms/test_enumeration.py
@@ -256,10 +256,10 @@ def test_get_genf(self, enum_verified):
x = sympy.Symbol("x")
expected_gf = -(
sympy.sqrt(
- -(4 * x ** 3 - 14 * x ** 2 + 8 * x - 1) / (2 * x ** 2 - 4 * x + 1)
+ -(4 * x**3 - 14 * x**2 + 8 * x - 1) / (2 * x**2 - 4 * x + 1)
)
- 1
- ) / (2 * x * (x ** 2 - 3 * x + 1))
+ ) / (2 * x * (x**2 - 3 * x + 1))
assert sympy.simplify(enum_verified.get_genf() - expected_gf) == 0
t = Tiling(
obstructions=[
@@ -318,18 +318,18 @@ def test_interleave_fixed_length(self, enum_verified):
cell_var = enum_verified._cell_variable((1, 0))
dummy_var = enum_verified._cell_variable((0, 0))
x = sympy.var("x")
- F = x ** 8 * track_var ** 3 * dummy_var ** 3
+ F = x**8 * track_var**3 * dummy_var**3
assert (
enum_verified._interleave_fixed_length(F, (1, 0), 1)
- == 4 * x ** 9 * dummy_var ** 3 * cell_var ** 1
+ == 4 * x**9 * dummy_var**3 * cell_var**1
)
assert (
enum_verified._interleave_fixed_length(F, (1, 0), 3)
- == 20 * x ** 11 * dummy_var ** 3 * cell_var ** 3
+ == 20 * x**11 * dummy_var**3 * cell_var**3
)
assert (
enum_verified._interleave_fixed_length(F, (1, 0), 0)
- == x ** 8 * dummy_var ** 3
+ == x**8 * dummy_var**3
)
def test_interleave_fixed_lengths(self, enum_verified):
@@ -337,30 +337,30 @@ def test_interleave_fixed_lengths(self, enum_verified):
cell_var = enum_verified._cell_variable((1, 0))
dummy_var = enum_verified._cell_variable((0, 0))
x = sympy.var("x")
- F = x ** 8 * track_var ** 3 * dummy_var ** 3
+ F = x**8 * track_var**3 * dummy_var**3
assert (
enum_verified._interleave_fixed_lengths(F, (1, 0), 1, 1)
- == 4 * x ** 9 * dummy_var ** 3 * cell_var ** 1
+ == 4 * x**9 * dummy_var**3 * cell_var**1
)
assert (
enum_verified._interleave_fixed_lengths(F, (1, 0), 3, 3)
- == 20 * x ** 11 * dummy_var ** 3 * cell_var ** 3
+ == 20 * x**11 * dummy_var**3 * cell_var**3
)
assert (
enum_verified._interleave_fixed_lengths(F, (1, 0), 0, 0)
- == x ** 8 * dummy_var ** 3
+ == x**8 * dummy_var**3
)
assert (
enum_verified._interleave_fixed_lengths(F, (1, 0), 0, 2)
- == x ** 8 * dummy_var ** 3
- + 4 * x ** 9 * dummy_var ** 3 * cell_var ** 1
- + 10 * x ** 10 * dummy_var ** 3 * cell_var ** 2
+ == x**8 * dummy_var**3
+ + 4 * x**9 * dummy_var**3 * cell_var**1
+ + 10 * x**10 * dummy_var**3 * cell_var**2
)
assert (
enum_verified._interleave_fixed_lengths(F, (1, 0), 1, 3)
- == 4 * x ** 9 * dummy_var ** 3 * cell_var ** 1
- + 10 * x ** 10 * dummy_var ** 3 * cell_var ** 2
- + 20 * x ** 11 * dummy_var ** 3 * cell_var ** 3
+ == 4 * x**9 * dummy_var**3 * cell_var**1
+ + 10 * x**10 * dummy_var**3 * cell_var**2
+ + 20 * x**11 * dummy_var**3 * cell_var**3
)
def test_genf_with_req(self):
@@ -399,11 +399,11 @@ def test_genf_with_big_finite_cell(self):
genf
== 1
+ 2 * x
- + 4 * x ** 2
- + 8 * x ** 3
- + 14 * x ** 4
- + 20 * x ** 5
- + 20 * x ** 6
+ + 4 * x**2
+ + 8 * x**3
+ + 14 * x**4
+ + 20 * x**5
+ + 20 * x**6
)
def test_with_two_reqs(self):
diff --git a/tests/algorithms/test_fusion.py b/tests/algorithms/test_fusion.py
index 0c153b86..1e7c8f2c 100644
--- a/tests/algorithms/test_fusion.py
+++ b/tests/algorithms/test_fusion.py
@@ -271,30 +271,6 @@ def test_fused_tiling(
GriddedPerm((1, 0), ((0, 1), (0, 1))),
]
)
- # We can get the fused tiling even for not fusable tilings
- assert col_fusion_big.fused_tiling() == Tiling(
- obstructions=[
- GriddedPerm((0,), ((0, 1),)),
- GriddedPerm((0,), ((0, 2),)),
- GriddedPerm((0,), ((0, 3),)),
- GriddedPerm((1, 0), ((0, 0), (0, 0))),
- GriddedPerm((1, 0), ((0, 1), (0, 1))),
- GriddedPerm((1, 0), ((0, 1), (0, 0))),
- GriddedPerm((1, 0), ((0, 0), (1, 0))),
- GriddedPerm((1, 0), ((0, 1), (1, 0))),
- GriddedPerm((1, 0), ((0, 1), (1, 1))),
- GriddedPerm((1, 0), ((1, 0), (1, 0))),
- GriddedPerm((1, 0), ((1, 1), (1, 0))),
- GriddedPerm((1, 0), ((1, 1), (1, 1))),
- GriddedPerm((1, 0), ((1, 2), (1, 0))),
- GriddedPerm((1, 0), ((1, 2), (1, 1))),
- GriddedPerm((1, 0), ((1, 2), (1, 2))),
- GriddedPerm((2, 1, 0), ((1, 3), (1, 3), (1, 0))),
- GriddedPerm((2, 1, 0), ((1, 3), (1, 3), (1, 1))),
- GriddedPerm((2, 1, 0), ((1, 3), (1, 3), (1, 2))),
- GriddedPerm((2, 1, 0), ((1, 3), (1, 3), (1, 3))),
- ]
- )
assert fusion_with_req.fused_tiling() == Tiling(
obstructions=[
GriddedPerm((0, 1), ((0, 0), (0, 0))),
diff --git a/tests/algorithms/test_obstruction_inferral.py b/tests/algorithms/test_obstruction_inferral.py
index b29db55a..bb82953a 100644
--- a/tests/algorithms/test_obstruction_inferral.py
+++ b/tests/algorithms/test_obstruction_inferral.py
@@ -173,6 +173,7 @@ def test_new_obs(self, obs_not_inf, obs_inf1, obs_inf2):
assert obs_not_inf.new_obs() == []
assert obs_inf2.new_obs() == [
GriddedPerm((0, 1), ((0, 0), (2, 0))),
+ GriddedPerm((0, 2, 1), ((0, 0), (0, 0), (2, 0))),
]
def test_obstruction_inferral(self, obs_inf2):
diff --git a/tests/algorithms/test_simplify_gridded_perms.py b/tests/algorithms/test_simplify_gridded_perms.py
index 5a432793..a67e025f 100644
--- a/tests/algorithms/test_simplify_gridded_perms.py
+++ b/tests/algorithms/test_simplify_gridded_perms.py
@@ -254,3 +254,141 @@ def test_reduce_to_join_of_subobs():
),
)
assert t == expected
+
+
+def test_minimal_req_duplicate():
+ assert Tiling(
+ obstructions=(
+ GriddedPerm((0, 2, 3, 1), ((0, 1), (0, 1), (0, 1), (1, 1))),
+ GriddedPerm((0, 2, 3, 1), ((0, 1), (0, 1), (1, 1), (1, 1))),
+ GriddedPerm((0, 2, 3, 1), ((0, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((0, 3, 2, 1), ((0, 1), (0, 1), (0, 1), (1, 1))),
+ GriddedPerm((0, 3, 2, 1), ((0, 1), (0, 1), (1, 1), (1, 1))),
+ GriddedPerm((0, 3, 2, 1), ((0, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 2, 3), ((1, 0), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 3, 2), ((1, 0), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 2, 0, 3), ((1, 0), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 2, 3, 0), ((1, 0), (1, 1), (1, 1), (1, 0))),
+ GriddedPerm((1, 3, 0, 2), ((1, 0), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 3, 2, 0), ((1, 0), (1, 1), (1, 1), (1, 0))),
+ GriddedPerm((2, 0, 3, 1), ((0, 1), (0, 1), (0, 1), (1, 1))),
+ GriddedPerm((2, 0, 3, 1), ((0, 1), (0, 1), (1, 1), (1, 1))),
+ GriddedPerm((2, 1, 0, 3), ((0, 1), (1, 0), (1, 0), (1, 1))),
+ GriddedPerm((2, 1, 0, 3), ((1, 1), (1, 0), (1, 0), (1, 1))),
+ GriddedPerm((2, 1, 3, 0), ((0, 1), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((2, 1, 3, 0), ((1, 1), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((1, 0, 3, 4, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 1), (1, 0), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 4, 3, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 0), (1, 0), (1, 1), (1, 0), (1, 0))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 1), (1, 0), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 3, 0, 4, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 3, 4, 0, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 0), (1, 0), (1, 1), (1, 0), (1, 0))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 4, 0, 3, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 0), (1, 1), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 4, 3, 0, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 0), (1, 1), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((0, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((0, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((0, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((0, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 0), (1, 0), (1, 1), (1, 0), (1, 0))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ ),
+ requirements=((GriddedPerm((1, 0), ((1, 0), (1, 0))),),),
+ assumptions=(),
+ ) == Tiling(
+ obstructions=(
+ GriddedPerm((0, 2, 3, 1), ((0, 1), (0, 1), (0, 1), (1, 1))),
+ GriddedPerm((0, 2, 3, 1), ((0, 1), (0, 1), (1, 1), (1, 1))),
+ GriddedPerm((0, 2, 3, 1), ((0, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((0, 3, 2, 1), ((0, 1), (0, 1), (0, 1), (1, 1))),
+ GriddedPerm((0, 3, 2, 1), ((0, 1), (0, 1), (1, 1), (1, 1))),
+ GriddedPerm((0, 3, 2, 1), ((0, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 2, 3), ((1, 0), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 3, 2), ((1, 0), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 2, 0, 3), ((1, 0), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 2, 3, 0), ((1, 0), (1, 1), (1, 1), (1, 0))),
+ GriddedPerm((1, 3, 0, 2), ((1, 0), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 3, 2, 0), ((1, 0), (1, 1), (1, 1), (1, 0))),
+ GriddedPerm((2, 0, 3, 1), ((0, 1), (0, 1), (0, 1), (1, 1))),
+ GriddedPerm((2, 0, 3, 1), ((0, 1), (0, 1), (1, 1), (1, 1))),
+ GriddedPerm((2, 1, 0, 3), ((0, 1), (1, 0), (1, 0), (1, 1))),
+ GriddedPerm((2, 1, 0, 3), ((1, 1), (1, 0), (1, 0), (1, 1))),
+ GriddedPerm((2, 1, 3, 0), ((0, 1), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((2, 1, 3, 0), ((1, 1), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((1, 0, 3, 4, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 1), (1, 0), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 3, 4, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 4, 3, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 0), (1, 0), (1, 1), (1, 0), (1, 0))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 1), (1, 0), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 0, 4, 3, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 3, 0, 4, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 3, 0, 4, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 3, 4, 0, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 0), (1, 0), (1, 1), (1, 0), (1, 0))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 3, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 4, 0, 3, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 0), (1, 1), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((1, 4, 0, 3, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((1, 4, 3, 0, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 0), (1, 1), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((1, 4, 3, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((0, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((0, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 0), (1, 0), (1, 0), (1, 1), (1, 0))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 1), (1, 1), (1, 0), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 0, 4, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((0, 1), (0, 1), (0, 1), (0, 1), (0, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((0, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((0, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 0), (1, 0), (1, 0), (1, 0), (1, 0))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 0), (1, 0), (1, 1), (1, 0), (1, 0))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 0), (1, 1))),
+ GriddedPerm((3, 1, 4, 0, 2), ((1, 1), (1, 1), (1, 1), (1, 1), (1, 1))),
+ ),
+ requirements=((GriddedPerm((1, 0), ((1, 0), (1, 0))),),),
+ assumptions=(),
+ )
diff --git a/tests/strategies/test_assumption_insertion.py b/tests/strategies/test_assumption_insertion.py
new file mode 100644
index 00000000..f5efac4c
--- /dev/null
+++ b/tests/strategies/test_assumption_insertion.py
@@ -0,0 +1,23 @@
+from tilings import GriddedPerm, Tiling
+from tilings.assumptions import ComponentAssumption, TrackingAssumption
+from tilings.strategies.assumption_insertion import AddAssumptionsStrategy
+
+
+def test_component_not_reversible():
+ ass = ComponentAssumption.from_cells([(0, 0)])
+ tiling = Tiling.from_string("123")
+ assert not AddAssumptionsStrategy([ass]).is_reversible(tiling)
+
+
+def test_cover_all_cells_reversible():
+ tiling = Tiling(
+ [
+ GriddedPerm.single_cell((0, 1), (0, 0)),
+ GriddedPerm.single_cell((0, 1), (0, 1)),
+ ]
+ )
+ ass1 = TrackingAssumption.from_cells([(0, 0), (0, 1)])
+ ass2 = TrackingAssumption.from_cells([(0, 0)])
+ assert AddAssumptionsStrategy([ass1]).is_reversible(tiling)
+ assert not AddAssumptionsStrategy([ass2]).is_reversible(tiling)
+ assert not AddAssumptionsStrategy([ass1, ass2]).is_reversible(tiling)
diff --git a/tests/strategies/test_constructor_equiv.py b/tests/strategies/test_constructor_equiv.py
index 812cc259..fa4ee902 100644
--- a/tests/strategies/test_constructor_equiv.py
+++ b/tests/strategies/test_constructor_equiv.py
@@ -1,7 +1,11 @@
+import pytest
+
+from comb_spec_searcher.strategies.rule import EquivalencePathRule
from tilings import GriddedPerm, Tiling, TrackingAssumption
from tilings.bijections import _TermCacher
from tilings.strategies.factor import FactorStrategy
from tilings.strategies.fusion import FusionStrategy
+from tilings.strategies.obstruction_inferral import ObstructionInferralFactory
from tilings.strategies.requirement_placement import RequirementPlacementStrategy
@@ -644,3 +648,28 @@ def test_req_placement_equiv_with_assumptions():
)
).constructor
)[0]
+
+
+def test_complement_multiple_params():
+ t = Tiling(
+ [
+ GriddedPerm.single_cell((0, 1, 2), (0, 0)),
+ GriddedPerm.single_cell((0, 1, 2), (1, 0)),
+ GriddedPerm((0, 1), ((0, 0), (1, 0))),
+ GriddedPerm((1, 0), ((0, 0), (1, 0))),
+ ],
+ [[GriddedPerm.point_perm((0, 0))]],
+ [
+ TrackingAssumption.from_cells([(0, 0), (1, 0)]),
+ TrackingAssumption.from_cells([(0, 0)]),
+ ],
+ )
+ for strategy in ObstructionInferralFactory()(t):
+ rule = strategy(t)
+ assert not rule.to_reverse_rule(0).is_equivalence()
+ with pytest.raises(AssertionError):
+ EquivalencePathRule([rule.to_reverse_rule(0)])
+
+ for i in range(4):
+ assert rule.sanity_check(i)
+ assert rule.to_reverse_rule(0).sanity_check(i)
diff --git a/tests/strategies/test_encoding.py b/tests/strategies/test_encoding.py
index 370fa24d..28274614 100644
--- a/tests/strategies/test_encoding.py
+++ b/tests/strategies/test_encoding.py
@@ -10,27 +10,39 @@
from tilings import GriddedPerm, TrackingAssumption
from tilings.strategies import (
AllPlacementsFactory,
+ AssumptionAndPointJumpingFactory,
+ AssumptionPointingFactory,
BasicVerificationStrategy,
CellInsertionFactory,
+ CellReductionFactory,
ComponentFusionFactory,
DatabaseVerificationStrategy,
+ DeflationFactory,
+ DummyStrategy,
ElementaryVerificationStrategy,
EmptyCellInferralFactory,
FactorFactory,
FactorInsertionFactory,
+ FusableRowAndColumnPlacementFactory,
FusionFactory,
+ InsertionEncodingVerificationStrategy,
LocallyFactorableVerificationStrategy,
LocalVerificationStrategy,
+ MonotoneSlidingFactory,
MonotoneTreeVerificationStrategy,
+ NoRootCellVerificationStrategy,
ObstructionInferralFactory,
ObstructionTransitivityFactory,
OneByOneVerificationStrategy,
PatternPlacementFactory,
+ PointingStrategy,
RearrangeAssumptionFactory,
+ RelaxAssumptionFactory,
RequirementCorroborationFactory,
RequirementExtensionFactory,
RequirementInsertionFactory,
RequirementPlacementFactory,
+ RequirementPointingFactory,
RootInsertionFactory,
RowAndColumnPlacementFactory,
RowColumnSeparationStrategy,
@@ -39,8 +51,15 @@
SplittingStrategy,
SubclassVerificationFactory,
SubobstructionInferralFactory,
+ SubobstructionInsertionFactory,
SymmetriesFactory,
+ TargetedCellInsertionFactory,
+ UnfusionColumnStrategy,
+ UnfusionFactory,
+ UnfusionRowStrategy,
)
+from tilings.strategies.cell_reduction import CellReductionStrategy
+from tilings.strategies.deflation import DeflationStrategy
from tilings.strategies.experimental_verification import SubclassVerificationStrategy
from tilings.strategies.factor import (
FactorStrategy,
@@ -48,7 +67,18 @@
FactorWithMonotoneInterleavingStrategy,
)
from tilings.strategies.fusion import ComponentFusionStrategy, FusionStrategy
+from tilings.strategies.monotone_sliding import GeneralizedSlidingStrategy
from tilings.strategies.obstruction_inferral import ObstructionInferralStrategy
+from tilings.strategies.point_jumping import (
+ AssumptionAndPointJumpingStrategy,
+ AssumptionJumpingStrategy,
+ PointJumpingStrategy,
+)
+from tilings.strategies.pointing import (
+ AssumptionPointingStrategy,
+ RequirementAssumptionPointingStrategy,
+ RequirementPointingStrategy,
+)
from tilings.strategies.rearrange_assumption import RearrangeAssumptionStrategy
from tilings.strategies.requirement_insertion import RequirementInsertionStrategy
from tilings.strategies.requirement_placement import RequirementPlacementStrategy
@@ -220,6 +250,26 @@ def partition_ignoreparent_workable(strategy):
]
+def partition_ignoreparent_workable_tracked(strategy):
+ return [
+ strategy(
+ partition=partition,
+ ignore_parent=ignore_parent,
+ workable=workable,
+ tracked=tracked,
+ )
+ for partition, ignore_parent, workable, tracked in product(
+ (
+ [[(2, 1), (0, 1)], [(1, 0)]],
+ (((0, 0), (0, 2)), ((0, 1),), ((3, 3), (4, 3))),
+ ),
+ (True, False),
+ (True, False),
+ (True, False),
+ )
+ ]
+
+
def gps_ignoreparent(strategy):
return [
strategy(gps=gps, ignore_parent=ignore_parent)
@@ -321,9 +371,7 @@ def sliding_strategy_arguments(strategy):
def short_length_arguments(strategy):
return [
- ShortObstructionVerificationStrategy(
- basis=basis, short_length=short_length, ignore_parent=ignore_parent
- )
+ strategy(basis=basis, short_length=short_length, ignore_parent=ignore_parent)
for short_length in range(4)
for ignore_parent in (True, False)
for basis in (
@@ -334,6 +382,15 @@ def short_length_arguments(strategy):
]
+def indices_and_row(strategy):
+ return [
+ strategy(idx1, idx2, row)
+ for idx1 in range(3)
+ for idx2 in range(3)
+ for row in (True, False)
+ ]
+
+
strategy_objects = (
maxreqlen_extrabasis_ignoreparent_one_cell_only(CellInsertionFactory)
+ ignoreparent(FactorInsertionFactory)
@@ -345,13 +402,14 @@ def short_length_arguments(strategy):
+ subreqs_partial_ignoreparent_dirs(RequirementPlacementFactory)
+ [SymmetriesFactory(), BasicVerificationStrategy(), EmptyCellInferralFactory()]
+ partition_ignoreparent_workable(FactorStrategy)
- + partition_ignoreparent_workable(FactorWithInterleavingStrategy)
- + partition_ignoreparent_workable(FactorWithMonotoneInterleavingStrategy)
+ + partition_ignoreparent_workable_tracked(FactorWithInterleavingStrategy)
+ + partition_ignoreparent_workable_tracked(FactorWithMonotoneInterleavingStrategy)
+ ignoreparent(DatabaseVerificationStrategy)
+ ignoreparent(LocallyFactorableVerificationStrategy)
+ ignoreparent(ElementaryVerificationStrategy)
+ ignoreparent(LocalVerificationStrategy)
+ ignoreparent(MonotoneTreeVerificationStrategy)
+ + ignoreparent(InsertionEncodingVerificationStrategy)
+ [ObstructionTransitivityFactory()]
+ [
OneByOneVerificationStrategy(
@@ -363,6 +421,16 @@ def short_length_arguments(strategy):
OneByOneVerificationStrategy(basis=[], ignore_parent=False, symmetry=False),
OneByOneVerificationStrategy(basis=None, ignore_parent=False, symmetry=False),
]
+ + [
+ NoRootCellVerificationStrategy(
+ basis=[Perm((0, 1, 2)), Perm((2, 1, 0, 3))], ignore_parent=True
+ ),
+ NoRootCellVerificationStrategy(
+ basis=[Perm((2, 1, 0, 3))], ignore_parent=False, symmetry=True
+ ),
+ NoRootCellVerificationStrategy(basis=[], ignore_parent=False, symmetry=False),
+ NoRootCellVerificationStrategy(basis=None, ignore_parent=False, symmetry=False),
+ ]
+ [
SubclassVerificationFactory(perms_to_check=[Perm((0, 1, 2)), Perm((1, 0))]),
SubclassVerificationFactory(perms_to_check=list(Perm.up_to_length(3))),
@@ -391,7 +459,21 @@ def short_length_arguments(strategy):
+ [ComponentFusionStrategy(row_idx=1)]
+ [ComponentFusionStrategy(col_idx=3)]
+ [ComponentFusionStrategy(col_idx=3)]
- + [FusionFactory()]
+ + [FusionFactory(tracked=True), FusionFactory(tracked=False)]
+ + [DeflationFactory(tracked=True), DeflationFactory(tracked=False)]
+ + [CellReductionFactory(tracked=True), DeflationFactory(tracked=False)]
+ + [
+ CellReductionStrategy((0, 0), True, True),
+ CellReductionStrategy((2, 1), True, False),
+ CellReductionStrategy((3, 3), False, True),
+ CellReductionStrategy((4, 1), False, False),
+ ]
+ + [
+ DeflationStrategy((0, 0), True, True),
+ DeflationStrategy((2, 1), True, False),
+ DeflationStrategy((3, 3), False, True),
+ DeflationStrategy((4, 1), False, False),
+ ]
+ [ComponentFusionFactory()]
+ [ObstructionInferralStrategy([GriddedPerm((0, 1, 2), ((0, 0), (1, 1), (1, 2)))])]
+ [
@@ -409,6 +491,48 @@ def short_length_arguments(strategy):
TrackingAssumption([GriddedPerm((0,), [(0, 0)])]),
)
]
+ + [AssumptionAndPointJumpingFactory()]
+ + indices_and_row(PointJumpingStrategy)
+ + indices_and_row(AssumptionJumpingStrategy)
+ + indices_and_row(AssumptionAndPointJumpingStrategy)
+ + [MonotoneSlidingFactory(), GeneralizedSlidingStrategy(1)]
+ + indices_and_row(PointJumpingStrategy)
+ + [TargetedCellInsertionFactory()]
+ + ignoreparent(SubobstructionInsertionFactory)
+ + [
+ AssumptionPointingFactory(),
+ DummyStrategy(),
+ FusableRowAndColumnPlacementFactory(),
+ PointingStrategy(),
+ RequirementPointingFactory(),
+ UnfusionRowStrategy(),
+ UnfusionColumnStrategy(),
+ UnfusionFactory(),
+ RelaxAssumptionFactory(),
+ ]
+ + [
+ AssumptionPointingStrategy(
+ TrackingAssumption(
+ [GriddedPerm((0,), [(0, 0)]), GriddedPerm((0,), [(1, 0)])]
+ )
+ )
+ ]
+ + [
+ RequirementPointingStrategy(
+ (
+ GriddedPerm.single_cell((0, 1), (0, 0)),
+ GriddedPerm.single_cell((0, 1), (0, 0)),
+ ),
+ (0, 1),
+ ),
+ RequirementAssumptionPointingStrategy(
+ (
+ GriddedPerm.single_cell((0, 1), (0, 0)),
+ GriddedPerm.single_cell((0, 1), (0, 0)),
+ ),
+ (0, 1),
+ ),
+ ]
)
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
diff --git a/tests/strategies/test_inferral.py b/tests/strategies/test_inferral.py
index 6504fa65..db57ec29 100644
--- a/tests/strategies/test_inferral.py
+++ b/tests/strategies/test_inferral.py
@@ -161,7 +161,10 @@ def test_obstruction_inferral(tiling1, tiling_not_inf):
requirements=((GriddedPerm((1, 0), ((1, 0), (1, 0))),),),
)
assert isinstance(rule.constructor, DisjointUnion)
- assert rule.formal_step == "added the obstructions {01: (0, 0), (0, 0)}"
+ assert (
+ rule.formal_step == "added the obstructions {01: (0, 0), (0, 0),"
+ " 021: (0, 0), (0, 0), (1, 0), 120: (0, 0), (0, 0), (1, 0)}"
+ )
assert rule.inferrable
assert not rule.possibly_empty
assert rule.ignore_parent
diff --git a/tests/strategies/test_reverse_fusion.py b/tests/strategies/test_reverse_fusion.py
index 04970c62..6b10775c 100644
--- a/tests/strategies/test_reverse_fusion.py
+++ b/tests/strategies/test_reverse_fusion.py
@@ -1,8 +1,13 @@
+import logging
+
import pytest
+from logzero import logger
from tilings import GriddedPerm, Tiling, TrackingAssumption
from tilings.strategies.fusion import FusionStrategy
+LOGGER = logging.getLogger(__name__)
+
def reverse_fusion_rules():
t = Tiling(
@@ -65,6 +70,33 @@ def reverse_fusion_rules():
yield FusionStrategy(col_idx=1, tracked=True)(t3).to_reverse_rule(0)
yield FusionStrategy(row_idx=1, tracked=True)(t3.rotate270()).to_reverse_rule(0)
+ left_requirement = [GriddedPerm.point_perm((1, 0))]
+ right_requirement = [GriddedPerm.point_perm((2, 0))]
+ t4 = t.add_assumptions([left_overlap, left]).add_list_requirement(right_requirement)
+ yield FusionStrategy(col_idx=1, tracked=True)(t4).to_reverse_rule(0)
+ t5 = t.add_assumptions([left_overlap, left]).add_list_requirement(left_requirement)
+ yield FusionStrategy(col_idx=1, tracked=True)(t5).to_reverse_rule(0)
+
+ # # FOR A MORE EXHAUSTIVE SET OF TESTS UNCOMMENT THE FOLLOWING
+ # from itertools import chain, combinations
+ #
+ # for assumptions in chain.from_iterable(
+ # combinations([left, right, left_overlap, right_overlap], i) for i in range(5)
+ # ):
+ # for reqs in chain.from_iterable(
+ # combinations([left_requirement, right_requirement], i) for i in range(2)
+ # ):
+ # tiling = t.add_assumptions(assumptions)
+ # for req in reqs:
+ # tiling = tiling.add_list_requirement(req)
+ # rule = FusionStrategy(col_idx=1, tracked=True)(tiling)
+ # rotate_tiling = tiling.rotate270()
+ # rotate_rule = FusionStrategy(row_idx=1, tracked=True)(rotate_tiling)
+ # yield rule
+ # if left in assumptions or right in assumptions:
+ # yield rule.to_reverse_rule(0)
+ # yield rotate_rule.to_reverse_rule(0)
+
@pytest.fixture
def both_reverse_fusion_rule():
@@ -89,7 +121,7 @@ def test_sanity_check(rule):
assert rule.sanity_check(length)
-def test_test_positive_reverse_fusion():
+def test_positive_reverse_fusion(caplog):
t = Tiling(
obstructions=[
GriddedPerm((0, 1), [(0, 0), (0, 0)]),
@@ -102,5 +134,8 @@ def test_test_positive_reverse_fusion():
rule = FusionStrategy(col_idx=0, tracked=True)(t)
assert rule.is_reversible()
reverse_rule = rule.to_reverse_rule(0)
- with pytest.raises(NotImplementedError):
- reverse_rule.sanity_check(4)
+ logger.propagate = True
+ with caplog.at_level(logging.WARNING):
+ assert reverse_rule.sanity_check(4)
+ assert len(caplog.records) == 1
+ assert "Skipping sanity checking generation" in caplog.text
diff --git a/tests/strategies/test_sliding.py b/tests/strategies/test_sliding.py
index dcf72df5..a0ef22c1 100644
--- a/tests/strategies/test_sliding.py
+++ b/tests/strategies/test_sliding.py
@@ -1,5 +1,6 @@
from tilings import GriddedPerm, Tiling
from tilings.assumptions import TrackingAssumption
+from tilings.strategies.monotone_sliding import MonotoneSlidingFactory
from tilings.strategies.sliding import SlidingFactory
tiling = Tiling(
@@ -31,6 +32,70 @@
),
)
+noslidetiling1 = Tiling(
+ obstructions=(
+ GriddedPerm((0, 1), ((0, 0), (0, 0))),
+ GriddedPerm((0, 1, 2), ((1, 0), (1, 0), (1, 0))),
+ GriddedPerm((0, 1, 2, 3), ((2, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 3, 2), ((2, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 2, 3, 4), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 4, 3), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 3, 2, 4), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 3, 4, 2), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 4, 2, 3), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 4, 3, 2), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 2, 3, 4, 1), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 2, 4, 3, 1), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (1, 0), (1, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (1, 0), (2, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (1, 0), (3, 0))),
+ GriddedPerm((0, 1, 2), ((1, 0), (1, 0), (2, 0))),
+ GriddedPerm((0, 1, 2), ((1, 0), (1, 0), (3, 0))),
+ GriddedPerm((0, 2, 1), ((0, 0), (1, 0), (3, 0))),
+ GriddedPerm((0, 2, 1), ((1, 0), (1, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((0, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 2, 3), ((0, 0), (2, 0), (2, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((0, 0), (2, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((1, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 2, 3), ((1, 0), (2, 0), (2, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((1, 0), (2, 0), (3, 0), (3, 0))),
+ ),
+ requirements=(),
+ assumptions=[TrackingAssumption((GriddedPerm((0,), ((0, 0),)),))],
+)
+
+noslidetiling2 = Tiling(
+ obstructions=(
+ GriddedPerm((0, 1), ((1, 0), (1, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (0, 0))),
+ GriddedPerm((0, 1, 2, 3), ((2, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 3, 2), ((2, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 2, 3, 4), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 4, 3), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 3, 2, 4), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 3, 4, 2), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 4, 2, 3), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 4, 3, 2), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 2, 3, 4, 1), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 2, 4, 3, 1), ((3, 0), (3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (1, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (2, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (3, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (1, 0), (2, 0))),
+ GriddedPerm((0, 1, 2), ((0, 0), (1, 0), (3, 0))),
+ GriddedPerm((0, 2, 1), ((0, 0), (0, 0), (3, 0))),
+ GriddedPerm((0, 2, 1), ((0, 0), (1, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((0, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 2, 3), ((0, 0), (2, 0), (2, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((0, 0), (2, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((1, 0), (2, 0), (2, 0), (2, 0))),
+ GriddedPerm((0, 1, 2, 3), ((1, 0), (2, 0), (2, 0), (3, 0))),
+ GriddedPerm((0, 1, 2, 3), ((1, 0), (2, 0), (3, 0), (3, 0))),
+ ),
+ requirements=(),
+ assumptions=[TrackingAssumption((GriddedPerm((0,), ((1, 0),)),))],
+)
+
def sanity_checker(rules):
found_some = False
@@ -46,3 +111,12 @@ def test_sliding_factory():
assert sanity_checker(SlidingFactory(True)(tiling.reverse()))
assert sanity_checker(SlidingFactory(True)(tiling.rotate90()))
assert sanity_checker(SlidingFactory(True)(tiling.rotate90().reverse()))
+
+
+def test_monotone_sliding_factory():
+ assert list(MonotoneSlidingFactory()(noslidetiling1)) == []
+ assert list(MonotoneSlidingFactory()(noslidetiling2)) == []
+ assert sanity_checker(MonotoneSlidingFactory()(tiling))
+ assert sanity_checker(MonotoneSlidingFactory()(tiling.rotate90()))
+ assert sanity_checker(MonotoneSlidingFactory()(tiling.rotate180()))
+ assert sanity_checker(MonotoneSlidingFactory()(tiling.rotate270()))
diff --git a/tests/strategies/test_verification.py b/tests/strategies/test_verification.py
index d78da88c..e605a037 100644
--- a/tests/strategies/test_verification.py
+++ b/tests/strategies/test_verification.py
@@ -18,9 +18,11 @@
LocallyFactorableVerificationStrategy,
LocalVerificationStrategy,
MonotoneTreeVerificationStrategy,
+ NoRootCellVerificationStrategy,
OneByOneVerificationStrategy,
ShortObstructionVerificationStrategy,
)
+from tilings.strategies.detect_components import DetectComponentsStrategy
from tilings.strategies.experimental_verification import SubclassVerificationStrategy
from tilings.tilescope import TileScopePack
@@ -192,7 +194,7 @@ def test_get_genf(self, strategy, enum_verified):
x = sympy.var("x")
assert (
sympy.simplify(
- strategy.get_genf(enum_verified[0]) - 1 / (2 * x ** 2 - 3 * x + 1)
+ strategy.get_genf(enum_verified[0]) - 1 / (2 * x**2 - 3 * x + 1)
)
== 0
)
@@ -468,7 +470,9 @@ def enum_not_verified(self, onebyone_enum):
def test_pack(self, strategy, enum_verified):
assert strategy.pack(
enum_verified[0]
- ) == TileScopePack.regular_insertion_encoding(3)
+ ) == TileScopePack.regular_insertion_encoding(3).add_initial(
+ DetectComponentsStrategy()
+ )
assert strategy.pack(enum_verified[1]).name == "factor pack"
assert strategy.pack(enum_verified[2]).name == "factor pack"
@@ -566,7 +570,7 @@ def test_get_specification(self, strategy, enum_verified):
def test_get_genf(self, strategy, enum_verified):
x = sympy.Symbol("x")
- expected_gf = (1 - x) / (4 * x ** 2 - 4 * x + 1)
+ expected_gf = (1 - x) / (4 * x**2 - 4 * x + 1)
assert sympy.simplify(strategy.get_genf(enum_verified[0]) - expected_gf) == 0
@@ -657,7 +661,9 @@ def test_pack(self, strategy, enum_verified):
strategy.pack(enum_verified[0])
assert strategy.pack(
enum_verified[1]
- ) == TileScopePack.regular_insertion_encoding(3)
+ ) == TileScopePack.regular_insertion_encoding(3).add_initial(
+ DetectComponentsStrategy()
+ )
@pytest.mark.timeout(30)
def test_get_specification(self, strategy, enum_verified):
@@ -673,10 +679,10 @@ def test_get_genf(self, strategy, enum_verified):
x = sympy.Symbol("x")
expected_gf = -(
sympy.sqrt(
- -(4 * x ** 3 - 14 * x ** 2 + 8 * x - 1) / (2 * x ** 2 - 4 * x + 1)
+ -(4 * x**3 - 14 * x**2 + 8 * x - 1) / (2 * x**2 - 4 * x + 1)
)
- 1
- ) / (2 * x * (x ** 2 - 3 * x + 1))
+ ) / (2 * x * (x**2 - 3 * x + 1))
assert sympy.simplify(strategy.get_genf(enum_verified[0]) - expected_gf) == 0
expected_gf = -1 / ((x - 1) * (x / (x - 1) + 1))
@@ -756,11 +762,11 @@ def test_genf_with_big_finite_cell(self, strategy):
genf
== 1
+ 2 * x
- + 4 * x ** 2
- + 8 * x ** 3
- + 14 * x ** 4
- + 20 * x ** 5
- + 20 * x ** 6
+ + 4 * x**2
+ + 8 * x**3
+ + 14 * x**4
+ + 20 * x**5
+ + 20 * x**6
)
def test_with_two_reqs(self, strategy):
@@ -1012,15 +1018,19 @@ def test_pack(self, strategy, enum_verified):
[Perm((0, 1, 2)), Perm((2, 3, 0, 1))]
)
- assert strategy.pack(enum_verified[5]) == TileScopePack.row_and_col_placements(
- row_only=True
- ).make_fusion(tracked=True).add_basis([Perm((0, 1, 2))])
+ # assert strategy.pack(
+ # enum_verified[5]
+ # ) == TileScopePack.row_and_col_placements(row_only=True).make_fusion(
+ # tracked=True
+ # ).add_basis(
+ # [Perm((0, 1, 2))]
+ # )
with pytest.raises(InvalidOperationError):
strategy.pack(enum_verified[6])
@pytest.mark.timeout(120)
def test_get_specification(self, strategy, enum_verified):
- for tiling in enum_verified[:-1]:
+ for tiling in enum_verified[:-2]:
spec = strategy.get_specification(tiling)
assert isinstance(spec, CombinatorialSpecification)
with pytest.raises(InvalidOperationError):
@@ -1100,12 +1110,6 @@ def test_132_with_two_points(self, strategy):
== -sympy.var("x") - 1
)
- def test_with_assumptions(self, strategy):
- ass = TrackingAssumption([GriddedPerm.point_perm((0, 0))])
- t = Tiling.from_string("01").add_assumption(ass)
- assert strategy.verified(t)
- assert strategy.get_genf(t) == sympy.sympify("-1/(k_0*x - 1)")
-
def test_with_123_subclass_12req(self, strategy):
t2 = Tiling(
obstructions=[
@@ -1147,6 +1151,113 @@ def test_subclass(self, strategy):
assert strategy.verified(Tiling.from_string("132_1234"))
+class TestNoRootCellVerificationStrategy(CommonTest):
+ @pytest.fixture
+ def strategy(self):
+ return NoRootCellVerificationStrategy(basis=[Perm((0, 1, 3, 2))])
+
+ @pytest.fixture
+ def formal_step(self):
+ return "tiling has no Av(0132) cell"
+
+ @pytest.fixture
+ def enum_verified(self):
+ # +-+-+-+-+
+ # |2| | | |
+ # +-+-+-+-+
+ # | | |●| |
+ # +-+-+-+-+
+ # |\| | | |
+ # +-+-+-+-+
+ # | |●| | |
+ # +-+-+-+-+
+ # |1| | |3|
+ # +-+-+-+-+
+ # 1: Av+(120, 0132)
+ # 2: Av(012)
+ # 3: Av(0132, 0231, 1203)
+ # \: Av(01)
+ # ●: point
+ # Crossing obstructions:
+ # 01: (0, 0), (3, 0)
+ # 012: (0, 0), (0, 0), (0, 2)
+ # 012: (0, 0), (0, 0), (0, 4)
+ # 012: (0, 0), (0, 2), (0, 4)
+ # 012: (0, 0), (0, 4), (0, 4)
+ # 012: (0, 2), (0, 4), (0, 4)
+ # 120: (0, 0), (0, 2), (0, 0)
+ # Requirement 0:
+ # 0: (0, 0)
+ # Requirement 1:
+ # 0: (1, 1)
+ # Requirement 2:
+ # 0: (2, 3)
+ return [
+ Tiling(
+ obstructions=(
+ GriddedPerm((0, 1), ((0, 0), (3, 0))),
+ GriddedPerm((0, 1), ((0, 2), (0, 2))),
+ GriddedPerm((0, 1), ((1, 1), (1, 1))),
+ GriddedPerm((0, 1), ((2, 3), (2, 3))),
+ GriddedPerm((1, 0), ((1, 1), (1, 1))),
+ GriddedPerm((1, 0), ((2, 3), (2, 3))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (0, 2))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (0, 4))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 2), (0, 4))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 4), (0, 4))),
+ GriddedPerm((0, 1, 2), ((0, 2), (0, 4), (0, 4))),
+ GriddedPerm((0, 1, 2), ((0, 4), (0, 4), (0, 4))),
+ GriddedPerm((1, 2, 0), ((0, 0), (0, 0), (0, 0))),
+ GriddedPerm((1, 2, 0), ((0, 0), (0, 2), (0, 0))),
+ GriddedPerm((0, 1, 3, 2), ((0, 0), (0, 0), (0, 0), (0, 0))),
+ GriddedPerm((0, 1, 3, 2), ((3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((0, 2, 3, 1), ((3, 0), (3, 0), (3, 0), (3, 0))),
+ GriddedPerm((1, 2, 0, 3), ((3, 0), (3, 0), (3, 0), (3, 0))),
+ ),
+ requirements=(
+ (GriddedPerm((0,), ((0, 0),)),),
+ (GriddedPerm((0,), ((1, 1),)),),
+ (GriddedPerm((0,), ((2, 3),)),),
+ ),
+ assumptions=(),
+ )
+ ]
+
+ @pytest.fixture
+ def enum_not_verified(self):
+ return [
+ Tiling.from_string("0132"),
+ Tiling(
+ obstructions=[
+ GriddedPerm.single_cell((0, 1, 3, 2), ((0, 0))),
+ GriddedPerm.single_cell((0, 2, 1), ((0, 1))),
+ GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (0, 1))),
+ ]
+ ),
+ ]
+
+ def test_get_genf(self, strategy, enum_verified):
+ pass
+
+ def test_children(self):
+ t2 = Tiling(
+ obstructions=[
+ GriddedPerm.single_cell((0, 2, 1), ((0, 0))),
+ GriddedPerm.single_cell((0, 2, 1), ((0, 1))),
+ GriddedPerm((0, 1, 3, 2), ((0, 0), (0, 0), (0, 1), (0, 1))),
+ ]
+ )
+ strategy = NoRootCellVerificationStrategy(basis=[Perm((0, 1, 3, 2))])
+ assert strategy(t2).children == ()
+
+ def test_change_basis(self):
+ strategy = NoRootCellVerificationStrategy()
+ strategy1 = strategy.change_basis([Perm((0, 1, 2))], False)
+ strategy2 = strategy1.change_basis([Perm((0, 1, 3, 2))], False)
+ assert strategy1.basis == (Perm((0, 1, 2)),)
+ assert strategy2.basis == (Perm((0, 1, 3, 2)),)
+
+
class TestShortObstructionVerificationStrategy(CommonTest):
@pytest.fixture
def strategy(self):
diff --git a/tests/test_bijections.py b/tests/test_bijections.py
index f1f3c07d..ad608090 100644
--- a/tests/test_bijections.py
+++ b/tests/test_bijections.py
@@ -4,6 +4,7 @@
import pytest
import requests
+from sympy import Point
from comb_spec_searcher import (
AtomStrategy,
@@ -20,7 +21,7 @@
FusionParallelSpecFinder,
_AssumptionPathTracker,
)
-from tilings.strategies import BasicVerificationStrategy
+from tilings.strategies import BasicVerificationStrategy, PointCorroborationFactory
from tilings.strategies.assumption_insertion import AddAssumptionsStrategy
from tilings.strategies.factor import FactorStrategy
from tilings.strategies.fusion import FusionStrategy
@@ -50,6 +51,7 @@ def find_bijection_between_fusion(
def _b2rc(basis: str) -> CombinatorialSpecificationSearcher:
pack = TileScopePack.row_and_col_placements(row_only=True)
+ pack = pack.remove_strategy(PointCorroborationFactory())
pack = pack.add_verification(BasicVerificationStrategy(), replace=True)
searcher = TileScope(basis, pack)
assert isinstance(searcher, CombinatorialSpecificationSearcher)
@@ -80,7 +82,8 @@ def _tester(basis1: str, basis2: str, max_size=7):
def _import_css_example():
r = requests.get(
"https://raw.githubusercontent.com/PermutaTriangle"
- "/comb_spec_searcher/develop/example.py"
+ "/comb_spec_searcher/develop/example.py",
+ timeout=5,
)
r.raise_for_status()
exec(r.text[: r.text.find("pack = StrategyPack(")], globals())
@@ -165,6 +168,7 @@ def test_bijection_8_cross_domain():
)
pack = TileScopePack.row_and_col_placements(row_only=True)
pack = pack.add_verification(BasicVerificationStrategy(), replace=True)
+ pack = pack.remove_strategy(PointCorroborationFactory())
pack.inferral_strats = ()
searcher1 = TileScope(t, pack)
@@ -230,9 +234,11 @@ def test_bijection_9_cross_domain():
def test_bijection_10():
pack1 = TileScopePack.requirement_placements()
pack1 = pack1.add_verification(BasicVerificationStrategy(), replace=True)
+ pack1 = pack1.remove_strategy(PointCorroborationFactory())
searcher1 = TileScope("132_4312", pack1)
pack2 = TileScopePack.requirement_placements()
pack2 = pack2.add_verification(BasicVerificationStrategy(), replace=True)
+ pack2 = pack2.remove_strategy(PointCorroborationFactory())
searcher2 = TileScope("132_4231", pack2)
_bijection_asserter(find_bijection_between(searcher1, searcher2))
@@ -453,6 +459,7 @@ def test_bijection_12():
def _pntrcpls(b1, b2):
pack = TileScopePack.point_and_row_and_col_placements(row_only=True)
pack = pack.add_verification(BasicVerificationStrategy(), replace=True)
+ pack = pack.remove_strategy(PointCorroborationFactory())
searcher1 = TileScope(b1, pack)
searcher2 = TileScope(b2, pack)
_bijection_asserter(find_bijection_between(searcher1, searcher2))
@@ -470,6 +477,7 @@ def _pntrcpls(b1, b2):
def test_bijection_13():
pack = TileScopePack.point_and_row_and_col_placements(row_only=True)
pack = pack.add_verification(BasicVerificationStrategy(), replace=True)
+ pack = pack.remove_strategy(PointCorroborationFactory())
searcher1 = TileScope("0132_0213_0231_0321_1032_1320_2031_2301_3021_3120", pack)
searcher2 = TileScope("0132_0213_0231_0312_0321_1302_1320_2031_2301_3120", pack)
_bijection_asserter(find_bijection_between(searcher1, searcher2))
@@ -489,11 +497,13 @@ def test_bijection_14_json():
def test_bijection_15_fusion():
pack = TileScopePack.row_and_col_placements(row_only=True).make_fusion(tracked=True)
pack = pack.add_verification(BasicVerificationStrategy(), replace=True)
+ pack = pack.remove_strategy(PointCorroborationFactory())
pack2 = TileScopePack.row_and_col_placements(row_only=True).make_fusion(
tracked=True
)
pack2 = pack2.add_initial(SlidingFactory(True))
pack2 = pack2.add_verification(BasicVerificationStrategy(), replace=True)
+ pack2 = pack2.remove_strategy(PointCorroborationFactory())
long_1234 = Perm(
(
47,
diff --git a/tests/test_classdb.py b/tests/test_classdb.py
new file mode 100644
index 00000000..d66a2508
--- /dev/null
+++ b/tests/test_classdb.py
@@ -0,0 +1,24 @@
+from tilings.assumptions import SkewComponentAssumption, TrackingAssumption
+from tilings.griddedperm import GriddedPerm
+from tilings.tilescope import TrackedClassDB
+from tilings.tiling import Tiling
+
+
+def test_tracked_classdb():
+ tiling = Tiling(
+ obstructions=(
+ GriddedPerm((1, 0, 2), ((1, 0), (1, 0), (1, 0))),
+ GriddedPerm((0, 2, 1, 3), ((0, 0), (0, 0), (0, 0), (0, 0))),
+ GriddedPerm((0, 2, 1, 3), ((0, 0), (0, 0), (0, 0), (1, 0))),
+ GriddedPerm((0, 2, 1, 3), ((0, 0), (0, 0), (1, 0), (1, 0))),
+ ),
+ requirements=(),
+ assumptions=(
+ SkewComponentAssumption((GriddedPerm((0,), ((1, 0),)),)),
+ TrackingAssumption((GriddedPerm((0,), ((1, 0),)),)),
+ ),
+ )
+ tracked_classdb = TrackedClassDB()
+ tracked_classdb.add(tiling)
+ new_tiling = tracked_classdb.get_class(0)
+ assert tiling == new_tiling
diff --git a/tests/test_griddedperm.py b/tests/test_griddedperm.py
index 36db022d..0d8666db 100644
--- a/tests/test_griddedperm.py
+++ b/tests/test_griddedperm.py
@@ -184,7 +184,8 @@ def test_is_isolated(simpleob, isolatedob):
def test_forced_point_index(singlecellob):
- assert singlecellob.forced_point_index((1, 1), DIR_SOUTH) is None
+ with pytest.raises(ValueError):
+ singlecellob.forced_point_index((1, 1), DIR_SOUTH)
assert singlecellob.forced_point_index((2, 2), DIR_WEST) == 0
assert singlecellob.forced_point_index((2, 2), DIR_SOUTH) == 1
assert singlecellob.forced_point_index((2, 2), DIR_NORTH) == 2
diff --git a/tests/test_strategy_pack.py b/tests/test_strategy_pack.py
index 53795fcf..a80975a4 100644
--- a/tests/test_strategy_pack.py
+++ b/tests/test_strategy_pack.py
@@ -6,7 +6,7 @@
from permuta import Perm
from permuta.misc import DIRS
from tilings import strategies as strat
-from tilings.strategies import SlidingFactory
+from tilings.strategies import AssumptionAndPointJumpingFactory, SlidingFactory
from tilings.strategy_pack import TileScopePack
@@ -70,9 +70,22 @@ def row_col_partial(pack):
]
+def length_row_col_partial(pack):
+ return [
+ pack(length=length, row_only=row_only, col_only=col_only, partial=partial)
+ for row_only, col_only, partial in product(
+ (True, False), (True, False), (True, False)
+ )
+ if not row_only or not col_only
+ for length in (1, 2, 3)
+ ]
+
+
packs = (
length(TileScopePack.all_the_strategies)
+ partial(TileScopePack.insertion_point_placements)
+ + partial(TileScopePack.subobstruction_placements)
+ + partial(TileScopePack.basis_pattern_insertions)
+ row_col_partial(TileScopePack.insertion_row_and_col_placements)
+ row_col_partial(TileScopePack.insertion_point_row_and_col_placements)
+ length_maxnumreq_partial(TileScopePack.only_root_placements)
@@ -87,6 +100,8 @@ def row_col_partial(pack):
+ length_partial(TileScopePack.requirement_placements)
+ row_col_partial(TileScopePack.row_and_col_placements)
+ length(TileScopePack.cell_insertions)
+ + length_row_col_partial(TileScopePack.point_and_row_and_col_placements)
+ + length_row_col_partial(TileScopePack.requirement_and_row_and_col_placements)
)
packs.extend(
@@ -100,6 +115,14 @@ def row_col_partial(pack):
+ [pack.make_interleaving() for pack in packs]
+ [pack.add_initial(SlidingFactory()) for pack in packs]
+ [pack.add_initial(SlidingFactory(use_symmetries=True)) for pack in packs]
+ + [pack.add_initial(AssumptionAndPointJumpingFactory()) for pack in packs]
+ + [
+ pack.kitchen_sinkify(
+ short_obs_len=4, obs_inferral_len=2, tracked=True, level=level
+ )
+ for pack in packs
+ for level in (1, 2, 3, 4, 5)
+ ]
)
diff --git a/tests/test_tilescope.py b/tests/test_tilescope.py
index e5a6a545..3ab0f3cd 100644
--- a/tests/test_tilescope.py
+++ b/tests/test_tilescope.py
@@ -1,3 +1,5 @@
+import gc
+
import pytest
import sympy
@@ -32,6 +34,20 @@
reginsenc = TileScopePack.regular_insertion_encoding(3)
+def collect_before(func):
+ """
+ Run gc collection before running the test.
+
+ This ensure that the collection ran in the test won't take to much time.
+ """
+
+ def inner():
+ gc.collect()
+ func()
+
+ return inner
+
+
@pytest.mark.timeout(20)
def test_132():
searcher = TileScope("132", point_placements)
@@ -114,6 +130,7 @@ def test_123():
@pytest.mark.timeout(120)
+@pytest.mark.skip(reason="Too inconsistent connection db")
def test_123_with_db():
searcher = TileScope("123", all_the_strategies_verify_database)
spec = searcher.auto_search(smallest=True)
@@ -283,6 +300,7 @@ def test_from_tiling():
assert sympy.simplify(spec.get_genf() - sympy.sympify("(1+x)/(1-x)")) == 0
+@collect_before
@pytest.mark.timeout(5)
def test_expansion():
"""
@@ -347,7 +365,8 @@ def test_domino():
]
-@pytest.mark.timeout(15)
+@collect_before
+@pytest.mark.timeout(60)
def test_parallel_forest():
expected_count = [1, 1, 2, 6, 22, 90, 394, 1806, 8558, 41586]
pack = TileScopePack.only_root_placements(2, 1)
@@ -360,6 +379,7 @@ def test_parallel_forest():
assert count == expected_count
+@collect_before
@pytest.mark.timeout(15)
def forest_expansion():
"""
diff --git a/tests/test_tiling.py b/tests/test_tiling.py
index dce056e2..53fdca0e 100644
--- a/tests/test_tiling.py
+++ b/tests/test_tiling.py
@@ -2212,29 +2212,7 @@ def test_partial_place_row(obs_inf_til):
def test_partial_place_col(obs_inf_til):
assert set(obs_inf_til.partial_place_col(0, 0)) == set(
[
- Tiling(
- obstructions=(
- GriddedPerm((0,), ((1, 0),)),
- GriddedPerm((0,), ((1, 2),)),
- GriddedPerm((0, 1), ((0, 1), (0, 1))),
- GriddedPerm((0, 1), ((0, 1), (1, 1))),
- GriddedPerm((0, 1), ((1, 1), (1, 1))),
- GriddedPerm((1, 0), ((0, 0), (0, 0))),
- GriddedPerm((1, 0), ((0, 1), (0, 1))),
- GriddedPerm((1, 0), ((0, 1), (1, 1))),
- GriddedPerm((1, 0), ((1, 1), (1, 1))),
- GriddedPerm((0, 2, 1), ((0, 0), (0, 2), (0, 2))),
- GriddedPerm((0, 3, 2, 1), ((0, 0), (0, 2), (0, 1), (0, 0))),
- GriddedPerm((0, 3, 2, 1), ((0, 1), (0, 2), (0, 2), (0, 2))),
- GriddedPerm((0, 3, 2, 1), ((0, 2), (0, 2), (0, 2), (0, 2))),
- GriddedPerm((1, 0, 3, 2), ((0, 2), (0, 1), (0, 2), (0, 2))),
- GriddedPerm((1, 0, 3, 2), ((0, 2), (0, 2), (0, 2), (0, 2))),
- ),
- requirements=(
- (GriddedPerm((0,), ((1, 1),)),),
- (GriddedPerm((1, 0), ((0, 1), (0, 0))),),
- ),
- ),
+ Tiling([GriddedPerm(tuple(), tuple())]),
Tiling(
obstructions=(
GriddedPerm((0,), ((1, 1),)),
@@ -2402,19 +2380,16 @@ def test_not_enumerable(self):
def test_enmerate_gp_up_to():
- assert (
- Tiling(
- obstructions=(
- GriddedPerm((0, 1), ((1, 2), (1, 2))),
- GriddedPerm((1, 0), ((1, 2), (1, 2))),
- GriddedPerm((0, 2, 1), ((0, 1), (0, 1), (0, 1))),
- GriddedPerm((0, 2, 1), ((2, 0), (2, 0), (2, 0))),
- ),
- requirements=((GriddedPerm((0,), ((1, 2),)),),),
- assumptions=(),
- ).enmerate_gp_up_to(8)
- == [0, 1, 2, 5, 14, 42, 132, 429, 1430]
- )
+ assert Tiling(
+ obstructions=(
+ GriddedPerm((0, 1), ((1, 2), (1, 2))),
+ GriddedPerm((1, 0), ((1, 2), (1, 2))),
+ GriddedPerm((0, 2, 1), ((0, 1), (0, 1), (0, 1))),
+ GriddedPerm((0, 2, 1), ((2, 0), (2, 0), (2, 0))),
+ ),
+ requirements=((GriddedPerm((0,), ((1, 2),)),),),
+ assumptions=(),
+ ).enmerate_gp_up_to(8) == [0, 1, 2, 5, 14, 42, 132, 429, 1430]
def test_column_reverse():
diff --git a/tilings/__init__.py b/tilings/__init__.py
index e0ff2d7b..f31c82f4 100644
--- a/tilings/__init__.py
+++ b/tilings/__init__.py
@@ -2,6 +2,6 @@
from tilings.griddedperm import GriddedPerm
from tilings.tiling import Tiling
-__version__ = "3.1.0"
+__version__ = "4.0.0"
__all__ = ["GriddedPerm", "Tiling", "TrackingAssumption"]
diff --git a/tilings/algorithms/enumeration.py b/tilings/algorithms/enumeration.py
index dcb5ccb7..c1bec536 100644
--- a/tilings/algorithms/enumeration.py
+++ b/tilings/algorithms/enumeration.py
@@ -1,12 +1,14 @@
import abc
from collections import deque
from itertools import chain
-from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, Optional
+from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Iterable, Optional
import requests
-from sympy import Expr, Function, Symbol, diff, simplify, sympify, var
+from sympy import Expr, Symbol, diff, simplify, sympify, var
from comb_spec_searcher.utils import taylor_expand
+from permuta import Av
+from permuta.permutils.symmetry import lex_min
from tilings.exception import InvalidOperationError
from tilings.griddedperm import GriddedPerm
from tilings.misc import is_tree
@@ -40,6 +42,7 @@ def get_genf(self, **kwargs) -> Expr:
"""
if not self.verified():
raise InvalidOperationError("The tiling is not verified")
+ raise NotImplementedError
def __repr__(self) -> str:
return "Enumeration for:\n" + str(self.tiling)
@@ -88,12 +91,12 @@ def _req_is_single_cell(req: Iterable[GriddedPerm]) -> bool:
all_cells = chain.from_iterable(gp.pos for gp in req_iter)
return all(c == cell for c in all_cells)
- def get_genf(self, **kwargs) -> Expr:
+ def get_genf(self, **kwargs) -> Any:
# pylint: disable=too-many-return-statements
if not self.verified():
raise InvalidOperationError("The tiling is not verified")
- funcs: Optional[Dict["Tiling", Function]] = kwargs.get("funcs")
+ funcs: Optional[Dict["Tiling", Any]] = kwargs.get("funcs")
if funcs is None:
funcs = {}
if self.tiling.requirements:
@@ -119,20 +122,18 @@ def get_genf(self, **kwargs) -> Expr:
return 1
if self.tiling == self.tiling.__class__.from_string("01_10"):
return 1 + x
- if self.tiling in (
- self.tiling.__class__.from_string("01"),
- self.tiling.__class__.from_string("10"),
- ):
- return 1 / (1 - x)
- if self.tiling in (
- self.tiling.__class__.from_string("123"),
- self.tiling.__class__.from_string("321"),
- ):
- return sympify("-1/2*(sqrt(-4*x + 1) - 1)/x")
- # TODO: should this create a spec as in the strategy?
- raise NotImplementedError(
- f"Look up the combopal database for:\n{self.tiling}"
- )
+ basis = [ob.patt for ob in self.tiling.obstructions]
+ basis_str = "_".join(map(str, lex_min(basis)))
+ uri = f"https://permpal.com/perms/raw_data_json/basis/{basis_str}"
+ request = requests.get(uri, timeout=10)
+ if request.status_code == 404:
+ raise NotImplementedError(f"No entry on permpal for {Av(basis)}")
+ data = request.json()
+ if data["generating_function_sympy"] is None:
+ raise NotImplementedError(
+ f"No explicit generating function on permpal for {Av(basis)}"
+ )
+ return sympify(data["generating_function_sympy"])
gf = None
if MonotoneTreeEnumeration(self.tiling).verified():
gf = MonotoneTreeEnumeration(self.tiling).get_genf()
@@ -203,7 +204,7 @@ def _visted_cells_aligned(self, cell, visited):
col_cells = self.tiling.cells_in_col(cell[0])
return (c for c in visited if (c in row_cells or c in col_cells))
- def get_genf(self, **kwargs) -> Expr:
+ def get_genf(self, **kwargs) -> Any:
# pylint: disable=too-many-locals
if not self.verified():
raise InvalidOperationError("The tiling is not verified")
@@ -242,7 +243,9 @@ def get_genf(self, **kwargs) -> Expr:
else:
F = self._interleave_fixed_lengths(F_tracked, cell, minlen, maxlen)
visited.add(cell)
- F = simplify(F.subs({v: 1 for v in F.free_symbols if v != x}))
+ F = simplify(
+ F.subs({v: 1 for v in F.free_symbols if v != x})
+ ) # type: ignore[operator]
# A simple test to warn us if the code is wrong
if __debug__:
lhs = taylor_expand(F, n=6)
@@ -289,11 +292,11 @@ def _interleave_fixed_length(self, F, cell, num_point):
`MonotoneTreeEnumeration._tracking_var` in `F`.
A variable is added to track the number of point in cell.
"""
- new_genf = self._tracking_var ** num_point * F
+ new_genf = self._tracking_var**num_point * F
for i in range(1, num_point + 1):
new_genf = diff(new_genf, self._tracking_var) / i
new_genf *= self._cell_variable(cell) ** num_point
- new_genf *= x ** num_point
+ new_genf *= x**num_point
return new_genf.subs({self._tracking_var: 1})
def _cell_num_point(self, cell):
@@ -331,7 +334,7 @@ class DatabaseEnumeration(Enumeration):
find the generating function and the minimal polynomial in the database.
"""
- API_ROOT_URL = "https://api.combopal.ru.is"
+ API_ROOT_URL = "https://api.permpal.com"
all_verified_tilings: FrozenSet[bytes] = frozenset()
num_verified_request = 0
@@ -345,7 +348,7 @@ def load_verified_tiling(cls):
"""
if not DatabaseEnumeration.all_verified_tilings:
uri = f"{cls.API_ROOT_URL}/all_verified_tilings"
- response = requests.get(uri)
+ response = requests.get(uri, timeout=10)
response.raise_for_status()
compressed_tilings = map(bytes.fromhex, response.json())
cls.all_verified_tilings = frozenset(compressed_tilings)
@@ -357,7 +360,7 @@ def _get_tiling_entry(self):
"""
key = self.tiling.to_bytes().hex()
search_url = f"{DatabaseEnumeration.API_ROOT_URL}/verified_tiling/key/{key}"
- r = requests.get(search_url)
+ r = requests.get(search_url, timeout=10)
if r.status_code == 404:
return None
r.raise_for_status()
@@ -377,7 +380,7 @@ def verified(self):
DatabaseEnumeration.load_verified_tiling()
return self._get_tiling_entry() is not None
- def get_genf(self, **kwargs) -> Expr:
+ def get_genf(self, **kwargs) -> Any:
if not self.verified():
raise InvalidOperationError("The tiling is not verified")
return sympify(self._get_tiling_entry()["genf"])
diff --git a/tilings/algorithms/factor.py b/tilings/algorithms/factor.py
index 294f2298..16ed9cf2 100644
--- a/tilings/algorithms/factor.py
+++ b/tilings/algorithms/factor.py
@@ -78,8 +78,8 @@ def _unite_assumptions(self) -> None:
"""
for assumption in self._tiling.assumptions:
if isinstance(assumption, ComponentAssumption):
- for comp in assumption.get_components(self._tiling):
- self._unite_cells(chain.from_iterable(gp.pos for gp in comp))
+ for cells in assumption.cell_decomposition(self._tiling):
+ self._unite_cells(cells)
else:
for gp in assumption.gps:
self._unite_cells(gp.pos)
diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py
index 61c16d17..f5092357 100644
--- a/tilings/algorithms/fusion.py
+++ b/tilings/algorithms/fusion.py
@@ -3,7 +3,16 @@
"""
import collections
from itertools import chain
-from typing import TYPE_CHECKING, Counter, Iterable, Iterator, List, Optional, Tuple
+from typing import (
+ TYPE_CHECKING,
+ Counter,
+ FrozenSet,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Tuple,
+)
from tilings.assumptions import (
ComponentAssumption,
@@ -278,15 +287,30 @@ def _can_fuse_assumption(
are all contained entirely on the left of the fusion region, entirely
on the right, or split in every possible way.
"""
- if isinstance(assumption, ComponentAssumption):
- return self.is_left_sided_assumption(
- assumption
- ) and self.is_right_sided_assumption(assumption)
+ left, right = self._left_fuse_region(), self._right_fuse_region()
+ cells = set(gp.pos[0] for gp in assumption.gps)
+ if (left.intersection(cells) and not left.issubset(cells)) or (
+ right.intersection(cells) and not right.issubset(cells)
+ ):
+ return False
return self._can_fuse_set_of_gridded_perms(fuse_counter) or (
- all(count == 1 for gp, count in fuse_counter.items())
- and self._is_one_sided_assumption(assumption)
+ not isinstance(assumption, ComponentAssumption)
+ and (
+ all(count == 1 for count in fuse_counter.values())
+ and self._is_one_sided_assumption(assumption)
+ )
)
+ def _left_fuse_region(self) -> FrozenSet[Cell]:
+ if self._fuse_row:
+ return self._tiling.cells_in_row(self._row_idx)
+ return self._tiling.cells_in_col(self._col_idx)
+
+ def _right_fuse_region(self) -> FrozenSet[Cell]:
+ if self._fuse_row:
+ return self._tiling.cells_in_row(self._row_idx + 1)
+ return self._tiling.cells_in_col(self._col_idx + 1)
+
def _is_one_sided_assumption(self, assumption: TrackingAssumption) -> bool:
"""
Return True if all of the assumption is contained either entirely on
@@ -401,6 +425,8 @@ def fused_tiling(self) -> "Tiling":
obstructions=self.obstruction_fuse_counter.keys(),
requirements=requirements,
assumptions=assumptions,
+ derive_empty=False,
+ already_minimized_obs=True,
)
return self._fused_tiling
@@ -431,7 +457,7 @@ def __init__(
):
if tiling.requirements:
raise NotImplementedError(
- "Component fusion does not handle " "requirements at the moment"
+ "Component fusion does not handle requirements at the moment"
)
super().__init__(
tiling,
@@ -492,7 +518,7 @@ def first_cell(self) -> Cell:
return self._first_cell
if not self._pre_check():
raise RuntimeError(
- "Pre-check failed. No component fusion " "possible and no first cell"
+ "Pre-check failed. No component fusion possible and no first cell"
)
assert self._first_cell is not None
return self._first_cell
@@ -507,7 +533,7 @@ def second_cell(self) -> Cell:
return self._second_cell
if not self._pre_check():
raise RuntimeError(
- "Pre-check failed. No component fusion " "possible and no second cell"
+ "Pre-check failed. No component fusion possible and no second cell"
)
assert self._second_cell is not None
return self._second_cell
@@ -567,21 +593,27 @@ def obstructions_to_add(self) -> Iterator[GriddedPerm]:
self.unfuse_gridded_perm(ob) for ob in self.obstruction_fuse_counter
)
- def _can_fuse_assumption(
- self, assumption: TrackingAssumption, fuse_counter: Counter[GriddedPerm]
- ) -> bool:
+ def _can_component_fuse_assumption(self, assumption: TrackingAssumption) -> bool:
"""
Return True if an assumption can be fused. That is, prefusion, the gps
- are all contained entirely on the left of the fusion region, entirely
- on the right, or split in every possible way.
+ do not touch the fuse region unless it is the correct sum or skew
+ assumption.
"""
- if not isinstance(assumption, ComponentAssumption):
- return self.is_left_sided_assumption(
- assumption
- ) and self.is_right_sided_assumption(assumption)
- return self._can_fuse_set_of_gridded_perms(fuse_counter) or (
- all(count == 1 for gp, count in fuse_counter.items())
- and self._is_one_sided_assumption(assumption)
+ gps = [
+ GriddedPerm.point_perm(self.first_cell),
+ GriddedPerm.point_perm(self.second_cell),
+ ]
+ return ( # if right type
+ (
+ isinstance(assumption, SumComponentAssumption)
+ and self.is_sum_component_fusion()
+ )
+ or (
+ isinstance(assumption, SkewComponentAssumption)
+ and self.is_skew_component_fusion()
+ ) # or covers whole region or none of it
+ or all(gp in assumption.gps for gp in gps)
+ or all(gp not in assumption.gps for gp in gps)
)
def _can_fuse_set_of_gridded_perms(
@@ -601,22 +633,48 @@ def fusable(self) -> bool:
return False
new_tiling = self._tiling.add_obstructions(self.obstructions_to_add())
- return self._tiling == new_tiling and self._check_isolation_level()
+ return (
+ self._tiling == new_tiling
+ and self._check_isolation_level()
+ and all(
+ self._can_component_fuse_assumption(assumption)
+ for assumption in self._tiling.assumptions
+ )
+ )
def new_assumption(self) -> ComponentAssumption:
"""
Return the assumption that needs to be counted in order to enumerate.
"""
fcell = self.first_cell
- scell = self.second_cell
gps = (GriddedPerm.single_cell((0,), fcell),)
+ if self.is_sum_component_fusion():
+ return SumComponentAssumption(gps)
+ return SkewComponentAssumption(gps)
+
+ def is_sum_component_fusion(self) -> bool:
+ """
+ Return true if is a sum component fusion
+ """
+ fcell = self.first_cell
+ scell = self.second_cell
if self._fuse_row:
sum_ob = GriddedPerm((1, 0), (scell, fcell))
else:
sum_ob = GriddedPerm((1, 0), (fcell, scell))
- if sum_ob in self._tiling.obstructions:
- return SumComponentAssumption(gps)
- return SkewComponentAssumption(gps)
+ return sum_ob in self._tiling.obstructions
+
+ def is_skew_component_fusion(self) -> bool:
+ """
+ Return true if is a skew component fusion
+ """
+ fcell = self.first_cell
+ scell = self.second_cell
+ if self._fuse_row:
+ skew_ob = GriddedPerm((0, 1), (fcell, scell))
+ else:
+ skew_ob = GriddedPerm((0, 1), (fcell, scell))
+ return skew_ob in self._tiling.obstructions
def __str__(self) -> str:
s = "ComponentFusion Algorithm for:\n"
diff --git a/tilings/algorithms/gridded_perm_generation.py b/tilings/algorithms/gridded_perm_generation.py
index 8110d4d6..bb8e4aa7 100644
--- a/tilings/algorithms/gridded_perm_generation.py
+++ b/tilings/algorithms/gridded_perm_generation.py
@@ -1,5 +1,5 @@
from heapq import heapify, heappop, heappush
-from typing import TYPE_CHECKING, Dict, List, Set, Tuple
+from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from tilings.griddedperm import GriddedPerm
@@ -32,17 +32,20 @@ class GriddedPermsOnTiling:
built by inserting points into the minimal gridded permutations.
"""
- def __init__(self, tiling: "Tiling"):
+ def __init__(self, tiling: "Tiling", yield_non_minimal: bool = False):
self._tiling = tiling
self._minimal_gps = MinimalGriddedPerms(
tiling.obstructions, tiling.requirements
)
+ self._yield_non_minimal = yield_non_minimal
self._yielded_gridded_perms: Set[GriddedPerm] = set()
def prepare_queue(self, size: int) -> List[QueuePacket]:
queue: List[QueuePacket] = []
heapify(queue)
- for mgp in self._minimal_gps.minimal_gridded_perms():
+ for mgp in self._minimal_gps.minimal_gridded_perms(
+ yield_non_minimal=self._yield_non_minimal
+ ):
if len(mgp) <= size:
packet = QueuePacket(mgp, (-1, -1), {}, 0)
heappush(queue, packet)
@@ -50,7 +53,7 @@ def prepare_queue(self, size: int) -> List[QueuePacket]:
break
return queue
- def gridded_perms(self, size: int, place_at_most: int = None):
+ def gridded_perms(self, size: int, place_at_most: Optional[int] = None):
if place_at_most is None:
place_at_most = size
queue = self.prepare_queue(size)
diff --git a/tilings/algorithms/gridded_perm_reduction.py b/tilings/algorithms/gridded_perm_reduction.py
index b520148f..990b0f6e 100644
--- a/tilings/algorithms/gridded_perm_reduction.py
+++ b/tilings/algorithms/gridded_perm_reduction.py
@@ -17,6 +17,7 @@ def __init__(
requirements: Tuple[Tuple[GriddedPerm, ...], ...],
sorted_input: bool = False,
already_minimized_obs: bool = False,
+ manual: bool = False,
):
# Only using MGP for typing purposes.
if sorted_input:
@@ -25,8 +26,8 @@ def __init__(
else:
self._obstructions = tuple(sorted(obstructions))
self._requirements = tuple(sorted(tuple(sorted(r)) for r in requirements))
-
- self._minimize_griddedperms(already_minimized_obs=already_minimized_obs)
+ if not manual:
+ self._minimize_griddedperms(already_minimized_obs=already_minimized_obs)
@property
def obstructions(self) -> Tuple[GriddedPerm, ...]:
@@ -95,22 +96,31 @@ def minimal_obs(self) -> bool:
Reduce the obstruction according to the requirements.
Return True if something changed.
"""
+ if not self._obstructions:
+ return False
changed = False
new_obs: Set[GriddedPerm] = set()
for requirement in self.requirements:
- new_obs.update(
+ cleaned_obs = tuple(
+ tuple(
+ GriddedPermReduction._minimize(
+ self.clean_isolated(self._obstructions, gp)
+ )
+ )
+ for gp in requirement
+ )
+ gpr = GriddedPermReduction(self._obstructions, cleaned_obs, manual=True)
+ gpr.minimal_reqs()
+ cleaned_obs = gpr.requirements
+ implied_obs = set(
MinimalGriddedPerms(
self._obstructions,
- tuple(
- tuple(
- GriddedPermReduction._minimize(
- self.clean_isolated(self._obstructions, gp)
- )
- )
- for gp in requirement
- ),
- ).minimal_gridded_perms()
+ cleaned_obs,
+ ).minimal_gridded_perms(
+ max_length_to_build=max(map(len, self._obstructions)) - 1
+ )
)
+ new_obs.update(implied_obs)
if new_obs:
changed = True
self._obstructions = tuple(
@@ -159,7 +169,7 @@ def remove_redundant(self, requirements: List[Requirement]) -> List[Requirement]
(
gps
for gps in islice(requirements, idx + 1, None)
- if gps != requirement
+ if sorted(gps) != sorted(requirement)
),
),
):
diff --git a/tilings/algorithms/locally_factorable_shift.py b/tilings/algorithms/locally_factorable_shift.py
index cb95b136..99452d1a 100644
--- a/tilings/algorithms/locally_factorable_shift.py
+++ b/tilings/algorithms/locally_factorable_shift.py
@@ -7,13 +7,15 @@
CombinatorialSpecification,
CombinatorialSpecificationSearcher,
)
-from comb_spec_searcher.strategies.constructor import CartesianProduct, DisjointUnion
+from comb_spec_searcher.strategies.constructor import DisjointUnion
from comb_spec_searcher.strategies.rule import Rule, VerificationRule
from comb_spec_searcher.strategies.strategy import VerificationStrategy
from comb_spec_searcher.strategies.strategy_pack import StrategyPack
from comb_spec_searcher.typing import CSSstrategy
from permuta import Av, Perm
from tilings import GriddedPerm, Tiling
+from tilings.strategies.detect_components import CountComponent
+from tilings.strategies.factor import FactorStrategy
__all__ = ["shift_from_spec"]
@@ -59,7 +61,31 @@ def expanded_spec(
"""
Return a spec where any tiling that does not have the basis in one cell is
verified.
+
+ A locally factorable tiling can always result in a spec where the
+ verified leaves are one by one if we remove the local verification
+ and monotone tree verification strategies and instead use the
+ interleaving factors. As we only care about the shift, we can
+ tailor our packs to find this.
"""
+ # pylint: disable=import-outside-toplevel
+ from tilings.strategies.verification import (
+ LocalVerificationStrategy,
+ MonotoneTreeVerificationStrategy,
+ )
+ from tilings.tilescope import TileScopePack
+
+ pack = TileScopePack(
+ initial_strats=pack.initial_strats,
+ inferral_strats=pack.inferral_strats,
+ expansion_strats=pack.expansion_strats,
+ ver_strats=pack.ver_strats,
+ name=pack.name,
+ )
+ pack = pack.remove_strategy(MonotoneTreeVerificationStrategy()).make_interleaving(
+ tracked=False, unions=False
+ )
+ pack = pack.remove_strategy(LocalVerificationStrategy())
pack = pack.add_verification(NoBasisVerification(symmetries), apply_first=True)
with TmpLoggingLevel(logging.WARN):
css = CombinatorialSpecificationSearcher(tiling, pack)
@@ -86,19 +112,25 @@ def traverse(t: Tiling) -> Optional[int]:
elif t.dimensions == (1, 1):
res = 0
elif isinstance(rule, VerificationRule):
- res = shift_from_spec(tiling, rule.pack(), symmetries)
- elif isinstance(rule, Rule) and isinstance(rule.constructor, DisjointUnion):
- children_reliance = [traverse(c) for c in rule.children]
- res = min([r for r in children_reliance if r is not None], default=None)
- elif isinstance(rule, Rule) and isinstance(rule.constructor, CartesianProduct):
+ raise ValueError(
+ "this should be unreachable, looks like JP, HU "
+ "and CB misunderstood the code."
+ )
+ elif isinstance(rule, Rule) and isinstance(rule.strategy, FactorStrategy):
min_points = [len(next(c.minimal_gridded_perms())) for c in rule.children]
point_sum = sum(min_points)
shifts = [point_sum - mpoint for mpoint in min_points]
children_reliance = [traverse(c) for c in rule.children]
res = min(
- [r + s for r, s in zip(children_reliance, shifts) if r is not None],
+ (r + s for r, s in zip(children_reliance, shifts) if r is not None),
default=None,
)
+ elif isinstance(rule, Rule) and isinstance(
+ rule.constructor, (DisjointUnion, CountComponent)
+ ):
+ children_reliance = [traverse(c) for c in rule.children]
+ res = min((r for r in children_reliance if r is not None), default=None)
+
else:
raise NotImplementedError(rule)
traverse_cache[t] = res
diff --git a/tilings/algorithms/minimal_gridded_perms.py b/tilings/algorithms/minimal_gridded_perms.py
index 8dd324c2..0d3e87ac 100644
--- a/tilings/algorithms/minimal_gridded_perms.py
+++ b/tilings/algorithms/minimal_gridded_perms.py
@@ -129,12 +129,44 @@ def contains(self, gp: GriddedPerm, *patts: GriddedPerm) -> bool:
return True
return False
- def _prepare_queue(self, queue: List[QueuePacket]) -> Iterator[GriddedPerm]:
+ def _product_requirements(
+ self, max_length_to_build: Optional[int] = None
+ ) -> Iterator[GPTuple]:
+ if max_length_to_build is None:
+ yield from product(*self.requirements)
+ return
+
+ def _rec_product_requirements(
+ requirements: Reqs, counter: Dict[Cell, int]
+ ) -> Iterator[GPTuple]:
+ if len(requirements) == 0:
+ yield tuple()
+ return
+ reqs = requirements[0]
+ rest = requirements[1:]
+ for gp in reqs:
+ gpcounter = Counter(gp.pos)
+ new_counter = Counter(
+ {
+ cell: max(gpcounter[cell], counter[cell])
+ for cell in chain(gpcounter, counter)
+ }
+ )
+ assert max_length_to_build is not None
+ if sum(new_counter.values()) <= max_length_to_build:
+ for gps in _rec_product_requirements(rest, new_counter):
+ yield (gp,) + gps
+
+ yield from _rec_product_requirements(self.requirements, Counter())
+
+ def _prepare_queue(
+ self, queue: List[QueuePacket], max_length_to_build: Optional[int] = None
+ ) -> Iterator[GriddedPerm]:
"""Add cell counters with gridded permutations to the queue.
The function yields all initial_gp that satisfy the requirements."""
if len(self.requirements) <= 1:
return
- for gps in product(*self.requirements):
+ for gps in self._product_requirements(max_length_to_build):
# try to stitch together as much of the independent cells of the
# gridded permutation together first
initial_gp = self.initial_gp(*gps)
@@ -379,7 +411,7 @@ def insert_point(
yield idx, nextgp
def minimal_gridded_perms(
- self, yield_non_minimal: bool = False
+ self, yield_non_minimal: bool = False, max_length_to_build: Optional[int] = None
) -> Iterator[GriddedPerm]:
"""
Yield all minimal gridded perms on the tiling.
@@ -388,6 +420,9 @@ def minimal_gridded_perms(
that are non-minimal, found by the initial_gp method. Even though it
may not be minimal, this is useful when trying to determine whether or
not a tiling is empty.
+
+ If `max_length_to_build` it will only try to build minimal gridded
+ perms of size shorter than this.
"""
if not self.requirements:
if GriddedPerm.empty_perm() not in self.obstructions:
@@ -404,7 +439,7 @@ def minimal_gridded_perms(
initial_gps_to_auto_yield: Dict[int, Set[GriddedPerm]] = defaultdict(set)
yielded: Set[GriddedPerm] = set()
- for gp in self._prepare_queue(queue):
+ for gp in self._prepare_queue(queue, max_length_to_build):
if yield_non_minimal:
yielded.add(gp)
yield gp
@@ -453,7 +488,9 @@ def _process_work_packet(
# perm containing it.
yielded.add(nextgp)
yield nextgp
- else:
+ elif (
+ max_length_to_build is None or len(nextgp) < max_length_to_build
+ ):
# Update the minimum index that we inserted a
# a point into each cell.
next_mindices = {
diff --git a/tilings/algorithms/obstruction_inferral.py b/tilings/algorithms/obstruction_inferral.py
index 5a401b38..aa90f8fe 100644
--- a/tilings/algorithms/obstruction_inferral.py
+++ b/tilings/algorithms/obstruction_inferral.py
@@ -2,6 +2,7 @@
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple
from tilings import GriddedPerm
+from tilings.algorithms.gridded_perm_generation import GriddedPermsOnTiling
if TYPE_CHECKING:
from tilings import Tiling
@@ -26,18 +27,36 @@ def potential_new_obs(self) -> Iterable[GriddedPerm]:
tiling if possible.
"""
- def new_obs(self) -> List[GriddedPerm]:
+ def new_obs(self, yield_non_minimal: bool = False) -> List[GriddedPerm]:
"""
Returns the list of new obstructions that can be added to the tiling.
"""
if self._new_obs is not None:
return self._new_obs
- newobs: List[GriddedPerm] = []
- for ob in sorted(self.potential_new_obs(), key=len):
- cont_newob = any(newob in ob for newob in newobs)
- if not cont_newob and self.can_add_obstruction(ob, self._tiling):
- newobs.append(ob)
- self._new_obs = newobs
+
+ perms_to_check = tuple(self.potential_new_obs())
+ if not perms_to_check:
+ self._new_obs = []
+ return self._new_obs
+
+ max_len_of_perms_to_check = max(map(len, perms_to_check))
+ max_length = (
+ self._tiling.maximum_length_of_minimum_gridded_perm()
+ + max_len_of_perms_to_check
+ )
+ GP = GriddedPermsOnTiling(
+ self._tiling, yield_non_minimal=yield_non_minimal
+ ).gridded_perms(max_length, place_at_most=max_len_of_perms_to_check)
+ perms_left = set(perms_to_check)
+ for gp in GP:
+ to_remove: List[GriddedPerm] = []
+ for perm in perms_left:
+ if gp.contains(perm):
+ to_remove.append(perm)
+ perms_left.difference_update(to_remove)
+ if not perms_left:
+ break
+ self._new_obs = sorted(perms_left)
return self._new_obs
@staticmethod
@@ -81,12 +100,12 @@ class AllObstructionInferral(ObstructionInferral):
obstruction of length up to obstruction_length which can be added.
"""
- def __init__(self, tiling: "Tiling", obstruction_length: int) -> None:
+ def __init__(self, tiling: "Tiling", obstruction_length: Optional[int]) -> None:
super().__init__(tiling)
self._obs_len = obstruction_length
@property
- def obstruction_length(self) -> int:
+ def obstruction_length(self) -> Optional[int]:
return self._obs_len
def not_required(self, gp: GriddedPerm) -> bool:
diff --git a/tilings/algorithms/obstruction_transitivity.py b/tilings/algorithms/obstruction_transitivity.py
index b32e390e..61246108 100644
--- a/tilings/algorithms/obstruction_transitivity.py
+++ b/tilings/algorithms/obstruction_transitivity.py
@@ -28,7 +28,7 @@ def __init__(self, tiling: "Tiling") -> None:
self._rowineq: Optional[Dict[int, Set[Tuple[int, int]]]] = None
self._positive_cells_col: Optional[Dict[int, List[int]]] = None
self._positive_cells_row: Optional[Dict[int, List[int]]] = None
- self._new_ineq = None
+ self._new_ineq: Optional[List[Tuple[Tuple[int, int], Tuple[int, int]]]] = None
def positive_cells_col(self, col_index: int) -> List[int]:
"""
@@ -136,9 +136,7 @@ def ineq_ob(ineq) -> GriddedPerm:
)
@staticmethod
- def ineq_closure(
- positive_cells: Iterable[Cell], ineqs: Set[Tuple[Cell, Cell]]
- ) -> Set[Tuple[Cell, Cell]]:
+ def ineq_closure(positive_cells: Iterable[int], ineqs: Set[Cell]) -> Set[Cell]:
"""
Computes the transitive closure over positive cells.
@@ -148,8 +146,8 @@ def ineq_closure(
The list of new inequalities is returned.
"""
- gtlist: Dict[Cell, List[Cell]] = defaultdict(list)
- ltlist: Dict[Cell, List[Cell]] = defaultdict(list)
+ gtlist: Dict[int, List[int]] = defaultdict(list)
+ ltlist: Dict[int, List[int]] = defaultdict(list)
for left, right in ineqs:
ltlist[left].append(right)
gtlist[right].append(left)
@@ -172,13 +170,15 @@ def ineq_closure(
to_analyse.add(gt)
return newineqs
- def new_ineq(self):
+ def new_ineq(
+ self,
+ ) -> List[Tuple[Tuple[int, int], Tuple[int, int]]]:
"""
Compute the new inequalities.
"""
if self._new_ineq is not None:
return self._new_ineq
- newineqs = []
+ newineqs: List[Tuple[Tuple[int, int], Tuple[int, int]]] = []
ncol, nrow = self._tiling.dimensions
for col in range(ncol):
ineqs = self.ineq_col(col)
diff --git a/tilings/algorithms/requirement_placement.py b/tilings/algorithms/requirement_placement.py
index 56f427e4..a3997518 100644
--- a/tilings/algorithms/requirement_placement.py
+++ b/tilings/algorithms/requirement_placement.py
@@ -1,7 +1,7 @@
-from itertools import chain
-from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, List, Tuple
+from itertools import chain, filterfalse
+from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, List, Optional, Tuple
-from permuta.misc import DIR_EAST, DIR_NORTH, DIR_SOUTH, DIR_WEST, DIRS
+from permuta.misc import DIR_EAST, DIR_NONE, DIR_NORTH, DIR_SOUTH, DIR_WEST, DIRS
from tilings import GriddedPerm
from tilings.assumptions import TrackingAssumption
@@ -301,6 +301,14 @@ def forced_obstructions_from_requirement(
"""
placed_cell = self._placed_cell(cell)
res = []
+ if cell in self._tiling.point_cells:
+ x, y = placed_cell
+ if self.own_row:
+ res.append(GriddedPerm.point_perm((x, y + 1)))
+ res.append(GriddedPerm.point_perm((x, y - 1)))
+ if self.own_col:
+ res.append(GriddedPerm.point_perm((x + 1, y)))
+ res.append(GriddedPerm.point_perm((x - 1, y)))
for idx, gp in zip(indices, gps):
# if cell is farther in the direction than gp[idx], then don't need
# to avoid any of the stretched grided perms
@@ -342,30 +350,46 @@ def place_point_of_gridded_permutation(
return self.place_point_of_req((gp,), (idx,), direction)[0]
def place_point_of_req(
- self, gps: Iterable[GriddedPerm], indices: Iterable[int], direction: Dir
+ self,
+ gps: Iterable[GriddedPerm],
+ indices: Iterable[int],
+ direction: Dir,
+ include_not: bool = False,
+ cells: Optional[Iterable[Cell]] = None,
) -> Tuple["Tiling", ...]:
"""
Return the tilings, where the placed point corresponds to the directionmost
(the furtest in the given direction, ex: leftmost point) of an occurrence
of any point idx, gp(idx) for gridded perms in gp, and idx in indices
"""
- cells = frozenset(gp.pos[idx] for idx, gp in zip(indices, gps))
+ if cells is not None:
+ cells = frozenset(cells)
+ else:
+ cells = frozenset(gp.pos[idx] for idx, gp in zip(indices, gps))
res = []
for cell in sorted(cells):
stretched = self._stretched_obstructions_requirements_and_assumptions(cell)
(obs, reqs, ass) = stretched
+ rem_req = self._remaining_requirement_from_requirement(gps, indices, cell)
+
+ if direction == DIR_NONE:
+ res.append(self._tiling.__class__(obs, reqs + [rem_req], ass))
+ if include_not:
+ res.append(self._tiling.__class__(obs + rem_req, reqs, ass))
+ continue
forced_obs = self.forced_obstructions_from_requirement(
gps, indices, cell, direction
)
-
+ forced_obs = [
+ o1
+ for o1 in forced_obs
+ if not any(o2 in o1 for o2 in filterfalse(o1.__eq__, forced_obs))
+ ]
reduced_obs = [o1 for o1 in obs if not any(o2 in o1 for o2 in forced_obs)]
- new_obs = reduced_obs + forced_obs
-
- rem_req = self._remaining_requirement_from_requirement(gps, indices, cell)
-
+ reduced_obs.extend(filterfalse(reduced_obs.__contains__, forced_obs))
res.append(
self._tiling.__class__(
- new_obs,
+ reduced_obs,
reqs + [rem_req],
assumptions=ass,
already_minimized_obs=True,
diff --git a/tilings/algorithms/row_col_separation.py b/tilings/algorithms/row_col_separation.py
index 606edb6e..bcf2ec43 100644
--- a/tilings/algorithms/row_col_separation.py
+++ b/tilings/algorithms/row_col_separation.py
@@ -398,7 +398,7 @@ def _separates_tiling(self, row_order, col_order):
)
@staticmethod
- def _get_cell_map(row_order, col_order):
+ def _get_cell_map(row_order, col_order) -> Dict[Cell, Cell]:
"""
Return the position of the according to the given row_order and
col_order.
@@ -406,13 +406,14 @@ def _get_cell_map(row_order, col_order):
This method does not account for any cleaning occuring in the initializer. For
the complete cell map use `get_cell_map`.
"""
- cell_map = {}
+ cell_map: Dict[Cell, Cell] = {}
+ row_map: Dict[Cell, int] = {}
for i, row in enumerate(row_order):
for cell in row:
- cell_map[cell] = (None, i)
+ row_map[cell] = i
for i, col in enumerate(col_order):
for cell in col:
- cell_map[cell] = (i, cell_map[cell][1])
+ cell_map[cell] = (i, row_map[cell])
return cell_map
def map_obstructions(self, cell_map):
diff --git a/tilings/algorithms/sliding.py b/tilings/algorithms/sliding.py
index c1552b10..2780e652 100644
--- a/tilings/algorithms/sliding.py
+++ b/tilings/algorithms/sliding.py
@@ -61,6 +61,9 @@ def slide_column(self, av_12: int, av_123: int) -> "Tiling":
requirements=self.tiling.requirements,
obstructions=obstructions,
assumptions=self._swap_assumptions(av_12, av_123),
+ derive_empty=False,
+ remove_empty_rows_and_cols=False,
+ simplify=False,
)
@staticmethod
diff --git a/tilings/assumptions.py b/tilings/assumptions.py
index f1f792bc..93ed2b9e 100644
--- a/tilings/assumptions.py
+++ b/tilings/assumptions.py
@@ -1,7 +1,7 @@
import abc
from importlib import import_module
from itertools import chain
-from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Tuple, Type
+from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Type
from permuta import Perm
@@ -25,7 +25,11 @@ def __init__(self, gps: Iterable[GriddedPerm]):
@classmethod
def from_cells(cls, cells: Iterable[Cell]) -> "TrackingAssumption":
gps = [GriddedPerm.single_cell((0,), cell) for cell in cells]
- return TrackingAssumption(gps)
+ return cls(gps)
+
+ def get_cells(self) -> Tuple[Cell, ...]:
+ assert all(len(gp) == 1 for gp in self.gps)
+ return tuple(gp.pos[0] for gp in self.gps)
def avoiding(
self,
@@ -142,15 +146,12 @@ def tiling_decomposition(self, tiling: "Tiling") -> List[List[Cell]]:
"""Return the components of a given tiling."""
@abc.abstractmethod
- def is_component(
- self,
- cells: List[Cell],
- point_cells: FrozenSet[Cell],
- positive_cells: FrozenSet[Cell],
- ) -> bool:
- """
- Return True if cells form a component.
- """
+ def opposite_tiling_decomposition(self, tiling: "Tiling") -> List[List[Cell]]:
+ """Return the alternative components of a given tiling."""
+
+ @abc.abstractmethod
+ def one_or_fewer_components(self, tiling: "Tiling", cell: Cell) -> bool:
+ """Return True if the cell contains one or fewer components."""
def get_components(self, tiling: "Tiling") -> List[List[GriddedPerm]]:
sub_tiling = tiling.sub_tiling(self.cells)
@@ -163,9 +164,39 @@ def get_components(self, tiling: "Tiling") -> List[List[GriddedPerm]]:
for cell in comp
]
for comp in components
- if self.is_component(
- comp, separated_tiling.point_cells, separated_tiling.positive_cells
+ if self.is_component(comp, separated_tiling)
+ ]
+
+ def is_component(self, cells: List[Cell], tiling: "Tiling") -> bool:
+ """
+ Return True if cells form a component on the tiling.
+
+ Cells are assumed to have come from cell_decomposition.
+ """
+ sub_tiling = tiling.sub_tiling(cells)
+ skew_cells = self.opposite_tiling_decomposition(sub_tiling)
+
+ if any(
+ scells[0] in sub_tiling.positive_cells
+ and self.one_or_fewer_components(sub_tiling, scells[0])
+ for scells in skew_cells
+ if len(scells) == 1
+ ):
+ return True
+
+ def is_positive(scells) -> bool:
+ return any(
+ all(any(cell in gp.pos for cell in scells) for gp in req)
+ for req in sub_tiling.requirements
)
+
+ return sum(1 for scells in skew_cells if is_positive(scells)) > 1
+
+ def cell_decomposition(self, tiling: "Tiling"):
+ sub_tiling = tiling.sub_tiling(self.cells)
+ return [
+ [sub_tiling.backward_map.map_cell(cell) for cell in comp]
+ for comp in self.tiling_decomposition(sub_tiling)
]
def get_value(self, gp: GriddedPerm) -> int:
@@ -191,25 +222,17 @@ def __hash__(self) -> int:
class SumComponentAssumption(ComponentAssumption):
- @staticmethod
- def decomposition(perm: Perm) -> List[Perm]:
+ def decomposition(self, perm: Perm) -> List[Perm]:
return perm.sum_decomposition() # type: ignore
- @staticmethod
- def tiling_decomposition(tiling: "Tiling") -> List[List[Cell]]:
+ def tiling_decomposition(self, tiling: "Tiling") -> List[List[Cell]]:
return tiling.sum_decomposition()
- @staticmethod
- def is_component(
- cells: List[Cell], point_cells: FrozenSet[Cell], positive_cells: FrozenSet[Cell]
- ) -> bool:
- if len(cells) == 2:
- (x1, y1), (x2, y2) = sorted(cells)
- if x1 != x2 and y1 > y2: # is skew
- return all(cell in positive_cells for cell in cells) or any(
- cell in point_cells for cell in cells
- )
- return False
+ def opposite_tiling_decomposition(self, tiling: "Tiling") -> List[List[Cell]]:
+ return tiling.skew_decomposition()
+
+ def one_or_fewer_components(self, tiling: "Tiling", cell: Cell) -> bool:
+ return GriddedPerm.single_cell(Perm((0, 1)), cell) in tiling.obstructions
def __str__(self):
return f"can count sum components in cells {self.cells}"
@@ -219,25 +242,17 @@ def __hash__(self) -> int:
class SkewComponentAssumption(ComponentAssumption):
- @staticmethod
- def decomposition(perm: Perm) -> List[Perm]:
+ def decomposition(self, perm: Perm) -> List[Perm]:
return perm.skew_decomposition() # type: ignore
- @staticmethod
- def tiling_decomposition(tiling: "Tiling") -> List[List[Cell]]:
+ def tiling_decomposition(self, tiling: "Tiling") -> List[List[Cell]]:
return tiling.skew_decomposition()
- @staticmethod
- def is_component(
- cells: List[Cell], point_cells: FrozenSet[Cell], positive_cells: FrozenSet[Cell]
- ) -> bool:
- if len(cells) == 2:
- (x1, y1), (x2, y2) = sorted(cells)
- if x1 != x2 and y1 < y2: # is sum
- return all(cell in positive_cells for cell in cells) or any(
- cell in point_cells for cell in cells
- )
- return False
+ def opposite_tiling_decomposition(self, tiling: "Tiling") -> List[List[Cell]]:
+ return tiling.sum_decomposition()
+
+ def one_or_fewer_components(self, tiling: "Tiling", cell: Cell) -> bool:
+ return GriddedPerm.single_cell(Perm((1, 0)), cell) in tiling.obstructions
def __str__(self):
return f"can count skew components in cells {self.cells}"
diff --git a/tilings/griddedperm.py b/tilings/griddedperm.py
index 212f65a6..fb4697fc 100644
--- a/tilings/griddedperm.py
+++ b/tilings/griddedperm.py
@@ -1,4 +1,5 @@
import json
+from array import array
from itertools import chain, combinations, islice, product, tee
from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, Tuple
@@ -121,6 +122,7 @@ def forced_point_index(self, cell: Cell, direction: int) -> int:
if direction == DIR_SOUTH:
return min((self._patt[idx], idx) for idx in indices)[1]
raise ValueError("You're lost, no valid direction")
+ raise ValueError("The gridded perm does not occupy the cell")
def forced_point_of_requirement(
self, gps: Tuple["GriddedPerm", ...], indices: Tuple[int, ...], direction: int
@@ -272,8 +274,8 @@ def all_subperms(self, proper: bool = True) -> Iterator["GriddedPerm"]:
)
def extend(self, c: int, r: int) -> Iterator["GriddedPerm"]:
- """Add n+1 to all possible positions in perm and all allowed positions given that
- placement."""
+ """Add n+1 to all possible positions in perm and all allowed positions given
+ that placement."""
n = len(self)
if n == 0:
yield from (
@@ -355,10 +357,10 @@ def compress(self) -> List[int]:
return list(chain(self._patt, chain.from_iterable(self._pos)))
@classmethod
- def decompress(cls, array: List[int]) -> "GriddedPerm":
+ def decompress(cls, arr: array) -> "GriddedPerm":
"""Decompresses a list of integers in the form outputted by the
compress method and constructs an Obstruction."""
- n, it = len(array) // 3, iter(array)
+ n, it = len(arr) // 3, iter(arr)
return cls(
Perm(next(it) for _ in range(n)), ((next(it), next(it)) for _ in range(n))
)
@@ -617,6 +619,10 @@ def to_svg(self, image_scale: float = 10.0) -> str:
"""Return the svg code to plot the GriddedPerm."""
i_scale = int(image_scale * 10)
val_to_pos, (m_x, m_y) = self._get_plot_pos()
+ points = " ".join(
+ f"{x * 10},{(m_y - y) * 10}"
+ for x, y in (val_to_pos[val] for val in self.patt)
+ )
return "".join(
[
(
@@ -641,15 +647,8 @@ def to_svg(self, image_scale: float = 10.0) -> str:
),
"\n",
(
- lambda path: (
- f'\n'
- )
- )(
- " ".join(
- f"{x * 10},{(m_y - y) * 10}"
- for x, y in (val_to_pos[val] for val in self.patt)
- )
+ f'\n'
),
"\n".join(
(
diff --git a/tilings/misc.py b/tilings/misc.py
index 4958db5d..a28ed316 100644
--- a/tilings/misc.py
+++ b/tilings/misc.py
@@ -3,7 +3,17 @@
useful.
"""
from functools import reduce
-from typing import Dict, Iterable, Iterator, Sequence, Set, Tuple, TypeVar
+from typing import (
+ Collection,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Sequence,
+ Set,
+ Tuple,
+ TypeVar,
+)
Vertex = TypeVar("Vertex")
T = TypeVar("T")
@@ -35,7 +45,9 @@ def intersection_reduce(iterables: Iterable[Iterable[T]]) -> Set[T]:
return set()
-def is_tree(vertices: Sequence[Vertex], edges: Sequence[Tuple[Vertex, Vertex]]) -> bool:
+def is_tree(
+ vertices: Collection[Vertex], edges: Collection[Tuple[Vertex, Vertex]]
+) -> bool:
"""
Return True if the undirected graph is a tree.
@@ -48,7 +60,7 @@ def is_tree(vertices: Sequence[Vertex], edges: Sequence[Tuple[Vertex, Vertex]])
def adjacency_table(
- vertices: Sequence[Vertex], edges: Sequence[Tuple[Vertex, Vertex]]
+ vertices: Collection[Vertex], edges: Collection[Tuple[Vertex, Vertex]]
) -> AdjTable:
"""Return adjacency table of edges."""
adj_table = {v: set() for v in vertices} # type: AdjTable
@@ -113,10 +125,10 @@ def partitions_iterator(lst: Sequence[T]) -> Iterator[Tuple[Tuple[T, ...], ...]]
yield tuple(map(tuple, part))
-def algorithm_u(ns, m):
+def algorithm_u(ns: Sequence[T], m: int):
# pylint: disable=too-many-statements,too-many-branches
def visit(n, a):
- ps = [[] for i in range(m)]
+ ps: List[List[T]] = [[] for i in range(m)]
for j in range(n):
ps[a[j + 1]].append(ns[j])
return ps
diff --git a/tilings/strategies/__init__.py b/tilings/strategies/__init__.py
index b4df26f9..00108ea9 100644
--- a/tilings/strategies/__init__.py
+++ b/tilings/strategies/__init__.py
@@ -1,30 +1,48 @@
-from .assumption_insertion import AddAssumptionFactory, AddInterleavingAssumptionFactory
+from .assumption_insertion import AddAssumptionFactory
from .assumption_splitting import SplittingStrategy
+from .cell_reduction import CellReductionFactory
+from .deflation import DeflationFactory
from .detect_components import DetectComponentsStrategy
+from .dummy_strategy import DummyStrategy
from .experimental_verification import (
+ NoRootCellVerificationStrategy,
ShortObstructionVerificationStrategy,
SubclassVerificationFactory,
)
from .factor import FactorFactory
from .fusion import ComponentFusionFactory, FusionFactory
+from .monotone_sliding import MonotoneSlidingFactory
from .obstruction_inferral import (
EmptyCellInferralFactory,
ObstructionInferralFactory,
ObstructionTransitivityFactory,
SubobstructionInferralFactory,
)
+from .point_jumping import AssumptionAndPointJumpingFactory
+from .pointing import (
+ AssumptionPointingFactory,
+ PointingStrategy,
+ RequirementPointingFactory,
+)
from .rearrange_assumption import RearrangeAssumptionFactory
+from .relax_assumption import RelaxAssumptionFactory
from .requirement_insertion import (
+ BasisPatternInsertionFactory,
CellInsertionFactory,
FactorInsertionFactory,
+ PointCorroborationFactory,
+ PositiveCorroborationFactory,
RemoveRequirementFactory,
RequirementCorroborationFactory,
RequirementExtensionFactory,
RequirementInsertionFactory,
RootInsertionFactory,
+ SubobstructionInsertionFactory,
+ TargetedCellInsertionFactory,
)
from .requirement_placement import (
AllPlacementsFactory,
+ FusableRowAndColumnPlacementFactory,
PatternPlacementFactory,
RequirementPlacementFactory,
RowAndColumnPlacementFactory,
@@ -32,8 +50,10 @@
from .row_and_col_separation import RowColumnSeparationStrategy
from .sliding import SlidingFactory
from .symmetry import SymmetriesFactory
+from .unfusion import UnfusionColumnStrategy, UnfusionFactory, UnfusionRowStrategy
from .verification import (
BasicVerificationStrategy,
+ ComponentVerificationStrategy,
DatabaseVerificationStrategy,
ElementaryVerificationStrategy,
InsertionEncodingVerificationStrategy,
@@ -46,14 +66,17 @@
__all__ = [
# Assumptions
"AddAssumptionFactory",
- "AddInterleavingAssumptionFactory",
"DetectComponentsStrategy",
"RearrangeAssumptionFactory",
"SplittingStrategy",
# Batch
+ "AllPlacementsFactory",
+ "BasisPatternInsertionFactory",
"CellInsertionFactory",
"FactorInsertionFactory",
- "AllPlacementsFactory",
+ "FusableRowAndColumnPlacementFactory",
+ "PointCorroborationFactory",
+ "PositiveCorroborationFactory",
"RemoveRequirementFactory",
"RequirementExtensionFactory",
"RequirementInsertionFactory",
@@ -61,11 +84,27 @@
"RequirementCorroborationFactory",
"RootInsertionFactory",
"RowAndColumnPlacementFactory",
+ "SubobstructionInsertionFactory",
+ "TargetedCellInsertionFactory",
# Decomposition
"FactorFactory",
+ # Deflation
+ "DeflationFactory",
+ # Derivatives
+ "AssumptionPointingFactory",
+ "PointingStrategy",
+ "RequirementPointingFactory",
+ "UnfusionColumnStrategy",
+ "UnfusionRowStrategy",
+ "UnfusionFactory",
# Equivalence
+ "MonotoneSlidingFactory",
"PatternPlacementFactory",
"SlidingFactory",
+ # Experimental
+ "AssumptionAndPointJumpingFactory",
+ "RelaxAssumptionFactory",
+ "DummyStrategy",
# Fusion
"ComponentFusionFactory",
"FusionFactory",
@@ -75,16 +114,20 @@
"ObstructionTransitivityFactory",
"RowColumnSeparationStrategy",
"SubobstructionInferralFactory",
+ # Reduction
+ "CellReductionFactory",
# Symmetry
"SymmetriesFactory",
# Verification
"BasicVerificationStrategy",
+ "ComponentVerificationStrategy",
"DatabaseVerificationStrategy",
"ElementaryVerificationStrategy",
"LocallyFactorableVerificationStrategy",
"LocalVerificationStrategy",
"InsertionEncodingVerificationStrategy",
"MonotoneTreeVerificationStrategy",
+ "NoRootCellVerificationStrategy",
"OneByOneVerificationStrategy",
"ShortObstructionVerificationStrategy",
"SubclassVerificationFactory",
diff --git a/tilings/strategies/assumption_insertion.py b/tilings/strategies/assumption_insertion.py
index 409fd06e..524f69f0 100644
--- a/tilings/strategies/assumption_insertion.py
+++ b/tilings/strategies/assumption_insertion.py
@@ -1,7 +1,7 @@
from collections import Counter
-from itertools import chain, product
+from itertools import product
from random import randint
-from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple
+from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple
from sympy import Eq, Expr, Function, Number, Symbol, var
@@ -19,11 +19,7 @@
Terms,
)
from tilings import GriddedPerm, Tiling
-from tilings.algorithms import FactorWithInterleaving
-from tilings.assumptions import TrackingAssumption
-from tilings.misc import partitions_iterator
-
-from .factor import assumptions_to_add, interleaving_rows_and_cols
+from tilings.assumptions import ComponentAssumption, TrackingAssumption
Cell = Tuple[int, int]
@@ -44,11 +40,11 @@ def __init__(
self.extra_parameters = extra_parameters
# the paramater that was added, to count we must sum over all possible values
self.new_parameters = tuple(new_parameters)
- self._child_param_map = self._build_child_param_map(parent, child)
+ self.child_param_map = self._build_child_param_map(parent, child)
def get_equation(self, lhs_func: Function, rhs_funcs: Tuple[Function, ...]) -> Eq:
rhs_func = rhs_funcs[0]
- subs: Dict[Symbol, Expr] = {
+ subs: Dict[Any, Expr] = {
var(child): var(parent) for parent, child in self.extra_parameters.items()
}
for k in self.new_parameters:
@@ -62,7 +58,7 @@ def get_terms(
self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
) -> Terms:
assert len(subterms) == 1
- return self._push_add_assumption(n, subterms[0], self._child_param_map)
+ return self._push_add_assumption(n, subterms[0], self.child_param_map)
@staticmethod
def _push_add_assumption(
@@ -94,7 +90,7 @@ def get_sub_objects(
self, subobjs: SubObjects, n: int
) -> Iterator[Tuple[Parameters, Tuple[List[Optional[GriddedPerm]], ...]]]:
for param, gps in subobjs[0](n).items():
- yield self._child_param_map(param), (gps,)
+ yield self.child_param_map(param), (gps,)
def random_sample_sub_objects(
self,
@@ -129,6 +125,96 @@ def equiv(
)
+class RemoveAssumptionsConstructor(Constructor):
+ """
+ The constructor used to count when a variable the same as n is removed.
+ """
+
+ def __init__(
+ self,
+ parent: Tiling,
+ child: Tiling,
+ parameter: str,
+ extra_parameters: Dict[str, str],
+ ):
+ # parent parameter -> child parameter mapping
+ self.extra_parameters = extra_parameters
+ # the paramater that was added, to count we must sum over all possible values
+ self.parameter = parameter
+ self.parameter_idx = parent.extra_parameters.index(self.parameter)
+ self.child_param_map = self._build_child_param_map(parent, child)
+
+ def get_equation(self, lhs_func: Function, rhs_funcs: Tuple[Function, ...]) -> Eq:
+ rhs_func = rhs_funcs[0]
+ subs: Dict[Symbol, Expr] = {
+ var(child): var(parent) for parent, child in self.extra_parameters.items()
+ }
+ subs[var("x")] = var("x") * var(self.parameter)
+ return Eq(lhs_func, rhs_func.subs(subs, simultaneous=True))
+
+ def reliance_profile(self, n: int, **parameters: int) -> RelianceProfile:
+ raise NotImplementedError
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ assert len(subterms) == 1
+ return self._push_add_assumption(n, subterms[0], self.child_param_map)
+
+ def _push_add_assumption(
+ self,
+ n: int,
+ child_terms: Callable[[int], Terms],
+ child_param_map: ParametersMap,
+ ) -> Terms:
+ new_terms: Terms = Counter()
+ for param, value in child_terms(n).items():
+ new_param = list(child_param_map(param))
+ new_param[self.parameter_idx] = n
+ new_terms[tuple(new_param)] += value
+ return new_terms
+
+ def _build_child_param_map(self, parent: Tiling, child: Tiling) -> ParametersMap:
+ parent_param_to_pos = {
+ param: pos for pos, param in enumerate(parent.extra_parameters)
+ }
+ child_param_to_parent_param = {v: k for k, v in self.extra_parameters.items()}
+ child_pos_to_parent_pos: Tuple[Tuple[int, ...], ...] = tuple(
+ (parent_param_to_pos[child_param_to_parent_param[param]],)
+ for param in child.extra_parameters
+ )
+ return self.build_param_map(
+ child_pos_to_parent_pos, len(parent.extra_parameters)
+ )
+
+ def get_sub_objects(
+ self, subobjs: SubObjects, n: int
+ ) -> Iterator[Tuple[Parameters, Tuple[List[Optional[GriddedPerm]], ...]]]:
+ raise NotImplementedError
+
+ def random_sample_sub_objects(
+ self,
+ parent_count: int,
+ subsamplers: SubSamplers,
+ subrecs: SubRecs,
+ n: int,
+ **parameters: int,
+ ):
+ raise NotImplementedError
+
+ def equiv(
+ self, other: "Constructor", data: Optional[object] = None
+ ) -> Tuple[bool, Optional[object]]:
+ return (
+ isinstance(other, type(self))
+ and len(other.parameter) == len(self.parameter)
+ and AddAssumptionsConstructor.extra_params_equiv(
+ (self.extra_parameters,), (other.extra_parameters,)
+ ),
+ None,
+ )
+
+
class AddAssumptionsStrategy(Strategy[Tiling, GriddedPerm]):
def __init__(self, assumptions: Iterable[TrackingAssumption], workable=False):
self.assumptions = tuple(set(assumptions))
@@ -139,21 +225,21 @@ def __init__(self, assumptions: Iterable[TrackingAssumption], workable=False):
workable=workable,
)
- @staticmethod
- def can_be_equivalent() -> bool:
+ def can_be_equivalent(self) -> bool:
return False
- @staticmethod
- def is_two_way(comb_class: Tiling):
+ def is_two_way(self, comb_class: Tiling):
return False
- @staticmethod
- def is_reversible(comb_class: Tiling) -> bool:
- return False
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return all(
+ not isinstance(assumption, ComponentAssumption)
+ and frozenset(gp.pos[0] for gp in assumption.gps) == comb_class.active_cells
+ for assumption in self.assumptions
+ )
- @staticmethod
def shifts(
- comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
) -> Tuple[int, ...]:
return (0,)
@@ -185,7 +271,23 @@ def reverse_constructor(
comb_class: Tiling,
children: Optional[Tuple[Tiling, ...]] = None,
) -> Constructor:
- raise NotImplementedError
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ assert idx == 0
+ child = children[idx]
+ assert len(self.assumptions) == 1
+ assumption = self.assumptions[0]
+ assert len(assumption.gps) == len(comb_class.active_cells)
+ parameter = child.get_assumption_parameter(self.assumptions[0])
+ extra_params = {
+ child.get_assumption_parameter(ass): comb_class.get_assumption_parameter(
+ ass
+ )
+ for ass in child.assumptions
+ if ass != assumption
+ }
+
+ return RemoveAssumptionsConstructor(child, comb_class, parameter, extra_params)
def extra_parameters(
self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
@@ -283,55 +385,3 @@ def to_jsonable(self) -> dict:
@classmethod
def from_dict(cls, d: dict) -> "AddAssumptionFactory":
return cls()
-
-
-class AddInterleavingAssumptionFactory(StrategyFactory[Tiling]):
- def __init__(self, unions: bool = False):
- self.unions = unions
-
- @staticmethod
- def strategy_from_components(
- comb_class: Tiling, components: Tuple[Tuple[Cell, ...], ...]
- ) -> Iterator[Rule]:
- """
- Yield an AddAssumption strategy for the given component if needed.
- """
- cols, rows = interleaving_rows_and_cols(components)
- assumptions = set(
- ass
- for ass in chain.from_iterable(
- assumptions_to_add(cells, cols, rows) for cells in components
- )
- if ass not in comb_class.assumptions
- )
- if assumptions:
- strategy = AddAssumptionsStrategy(assumptions, workable=True)
- yield strategy(comb_class)
-
- # TODO: monotone?
- def __call__(self, comb_class: Tiling) -> Iterator[Rule]:
- factor_algo = FactorWithInterleaving(comb_class)
- if factor_algo.factorable():
- min_comp = tuple(tuple(part) for part in factor_algo.get_components())
- if self.unions:
- for partition in partitions_iterator(min_comp):
- components = tuple(
- tuple(chain.from_iterable(part)) for part in partition
- )
- yield from self.strategy_from_components(comb_class, components)
- yield from self.strategy_from_components(comb_class, min_comp)
-
- def __repr__(self) -> str:
- return self.__class__.__name__ + "()"
-
- def __str__(self) -> str:
- return "add interleaving assumptions to factor"
-
- def to_jsonable(self) -> dict:
- d: dict = super().to_jsonable()
- d["unions"] = self.unions
- return d
-
- @classmethod
- def from_dict(cls, d: dict) -> "AddInterleavingAssumptionFactory":
- return cls(**d)
diff --git a/tilings/strategies/assumption_splitting.py b/tilings/strategies/assumption_splitting.py
index 71610128..e82cb1bc 100644
--- a/tilings/strategies/assumption_splitting.py
+++ b/tilings/strategies/assumption_splitting.py
@@ -2,7 +2,7 @@
from functools import reduce
from itertools import product
from operator import mul
-from typing import Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
+from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
from sympy import Eq, Function, var
@@ -31,7 +31,7 @@
class Split(Constructor):
"""
- The constructor used to cound when a variable is counted by some multiple
+ The constructor used to count when a variable is counted by some multiple
disjoint subvariables.
"""
@@ -40,7 +40,7 @@ def __init__(self, split_parameters: Dict[str, Tuple[str, ...]]):
def get_equation(self, lhs_func: Function, rhs_funcs: Tuple[Function, ...]) -> Eq:
rhs_func = rhs_funcs[0]
- subs: Dict[var, List[var]] = defaultdict(list)
+ subs: Dict[Any, List[Any]] = defaultdict(list)
for parent, children in self.split_parameters.items():
for child in children:
subs[var(child)].append(var(parent))
@@ -156,16 +156,13 @@ def __init__(
workable=workable,
)
- @staticmethod
- def can_be_equivalent() -> bool:
+ def can_be_equivalent(self) -> bool:
return False
- @staticmethod
- def is_two_way(comb_class: Tiling):
+ def is_two_way(self, comb_class: Tiling):
return False
- @staticmethod
- def is_reversible(comb_class: Tiling):
+ def is_reversible(self, comb_class: Tiling):
return False
def shifts(
@@ -186,9 +183,17 @@ def decomposition_function(self, comb_class: Tiling) -> Optional[Tuple[Tiling]]:
new_assumptions: List[TrackingAssumption] = []
for ass in comb_class.assumptions:
new_assumptions.extend(self._split_assumption(ass, components))
- return (
- Tiling(comb_class.obstructions, comb_class.requirements, new_assumptions),
+ new_tiling = Tiling(
+ comb_class.obstructions,
+ comb_class.requirements,
+ sorted(new_assumptions),
+ remove_empty_rows_and_cols=False,
+ derive_empty=False,
+ simplify=False,
+ sorted_input=True,
)
+ new_tiling.clean_assumptions()
+ return (new_tiling,)
def _split_assumption(
self, assumption: TrackingAssumption, components: Tuple[Set[Cell], ...]
@@ -305,8 +310,7 @@ def reverse_constructor(
) -> Constructor:
raise NotImplementedError
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "splitting the assumptions"
def backward_map(
diff --git a/tilings/strategies/cell_reduction.py b/tilings/strategies/cell_reduction.py
new file mode 100644
index 00000000..9ff68ea9
--- /dev/null
+++ b/tilings/strategies/cell_reduction.py
@@ -0,0 +1,307 @@
+"""The cell reduction strategy."""
+from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple, cast
+
+import sympy
+
+from comb_spec_searcher import Constructor, Strategy, StrategyFactory
+from comb_spec_searcher.exception import StrategyDoesNotApply
+from comb_spec_searcher.typing import (
+ Parameters,
+ RelianceProfile,
+ SubObjects,
+ SubRecs,
+ SubSamplers,
+ SubTerms,
+ Terms,
+)
+from permuta import Perm
+from tilings import GriddedPerm, Tiling
+from tilings.assumptions import ComponentAssumption, TrackingAssumption
+
+Cell = Tuple[int, int]
+
+
+class CellReductionConstructor(Constructor):
+ def __init__(self, parameter: str):
+ self.parameter = parameter
+
+ def get_equation(
+ self, lhs_func: sympy.Function, rhs_funcs: Tuple[sympy.Function, ...]
+ ) -> sympy.Eq:
+ raise NotImplementedError
+
+ def reliance_profile(self, n: int, **parameters: int) -> RelianceProfile:
+ raise NotImplementedError
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ raise NotImplementedError
+
+ def get_sub_objects(
+ self, subobjs: SubObjects, n: int
+ ) -> Iterator[Tuple[Parameters, Tuple[List[Optional[GriddedPerm]]]]]:
+ raise NotImplementedError
+
+ def random_sample_sub_objects(
+ self,
+ parent_count: int,
+ subsamplers: SubSamplers,
+ subrecs: SubRecs,
+ n: int,
+ **parameters: int,
+ ) -> Tuple[GriddedPerm]:
+ raise NotImplementedError
+
+ def equiv(
+ self, other: "Constructor", data: Optional[object] = None
+ ) -> Tuple[bool, Optional[object]]:
+ raise NotImplementedError
+
+
+class CellReductionStrategy(Strategy[Tiling, GriddedPerm]):
+ """A strategy that replaces the cell with an increasing or decreasing cell."""
+
+ def __init__(
+ self,
+ cell: Cell,
+ increasing: bool,
+ tracked: bool = True,
+ ):
+ self.cell = cell
+ self.tracked = tracked
+ self.increasing = increasing
+ super().__init__()
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return False
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return False
+
+ def shifts(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[int, ...]:
+ return (0, 0)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, Tiling]:
+ if self.increasing:
+ extra = Perm((1, 0))
+ else:
+ extra = Perm((0, 1))
+ reduced_obs = sorted(
+ [
+ ob
+ for ob in comb_class.obstructions
+ if not ob.pos[0] == self.cell or not ob.is_single_cell()
+ ]
+ + [GriddedPerm.single_cell(extra, self.cell)]
+ )
+ reduced_reqs = sorted(
+ req
+ for req in comb_class.requirements
+ if not all(gp.pos[0] == self.cell and gp.is_single_cell() for gp in req)
+ )
+ reduced_ass = sorted(
+ ass
+ for ass in comb_class.assumptions
+ if not isinstance(ass, ComponentAssumption)
+ or GriddedPerm.point_perm(self.cell) not in ass.gps
+ )
+ reduced_tiling = Tiling(
+ reduced_obs,
+ reduced_reqs,
+ reduced_ass,
+ remove_empty_rows_and_cols=False,
+ derive_empty=False,
+ simplify=False,
+ sorted_input=True,
+ )
+ local_basis = comb_class.sub_tiling([self.cell])
+ if self.tracked:
+ return (
+ reduced_tiling.add_assumption(
+ TrackingAssumption.from_cells([self.cell])
+ ),
+ local_basis,
+ )
+ return reduced_tiling, local_basis
+
+ def constructor(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> CellReductionConstructor:
+ if not self.tracked:
+ raise NotImplementedError("The reduction strategy was not tracked")
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Can't reduce the cell")
+ ass = TrackingAssumption.from_cells([self.cell])
+ child_param = children[0].get_assumption_parameter(ass)
+ gp = GriddedPerm.point_perm(self.cell)
+ if any(gp in assumption.gps for assumption in comb_class.assumptions):
+ raise NotImplementedError
+ return CellReductionConstructor(child_param)
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Can't reduce the cell")
+ raise NotImplementedError
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str]]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Strategy does not apply")
+ raise NotImplementedError
+
+ def formal_step(self) -> str:
+ return f"reducing cell {self.cell}"
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def to_jsonable(self) -> dict:
+ d: dict = super().to_jsonable()
+ d.pop("ignore_parent")
+ d.pop("inferrable")
+ d.pop("possibly_empty")
+ d.pop("workable")
+ d["cell"] = self.cell
+ d["tracked"] = self.tracked
+ d["increasing"] = self.increasing
+ return d
+
+ def __repr__(self) -> str:
+ args = ", ".join(
+ [
+ f"cell={self.cell!r}",
+ f"increasing={self.increasing!r}",
+ f"tracked={self.tracked!r}",
+ ]
+ )
+ return f"{self.__class__.__name__}({args})"
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "CellReductionStrategy":
+ cell = cast(Tuple[int, int], tuple(d.pop("cell")))
+ tracked = d.pop("tracked")
+ increasing = d.pop("increasing")
+ assert not d
+ return cls(cell, increasing, tracked)
+
+ @staticmethod
+ def get_eq_symbol() -> str:
+ return "↣"
+
+
+class CellReductionFactory(StrategyFactory[Tiling]):
+ def __init__(self, tracked: bool):
+ self.tracked = tracked
+ super().__init__()
+
+ def __call__(self, comb_class: Tiling) -> Iterator[CellReductionStrategy]:
+ if comb_class.dimensions == (1, 1):
+ return
+ cell_bases = comb_class.cell_basis()
+ for cell in self.reducible_cells(comb_class):
+ if not ( # a finite cell
+ any(patt.is_increasing() for patt in cell_bases[cell][0])
+ and any(patt.is_decreasing() for patt in cell_bases[cell][0])
+ ) and self.can_reduce_cell_with_requirements_and_assumptions(
+ comb_class, cell
+ ):
+ yield CellReductionStrategy(cell, True, self.tracked)
+ yield CellReductionStrategy(cell, False, self.tracked)
+
+ def can_reduce_cell_with_requirements_and_assumptions(
+ self, tiling: Tiling, cell: Cell
+ ) -> bool:
+ return all( # local
+ all(gp.pos[0] == cell and gp.is_single_cell() for gp in req)
+ or all(
+ # at most one point in cell
+ sum(1 for _ in gp.points_in_cell(cell)) < 2
+ # no gp in row and col
+ and not self.gp_in_row_and_col(gp, cell)
+ for gp in req
+ )
+ for req in tiling.requirements
+ ) and all(
+ not isinstance(ass, ComponentAssumption)
+ or GriddedPerm.point_perm(cell) not in ass.gps
+ or len(ass.gps) == 1
+ for ass in tiling.assumptions
+ )
+
+ @staticmethod
+ def gp_in_row_and_col(gp: GriddedPerm, cell: Cell) -> bool:
+ """Return True if there are points touching a cell in the row and col of
+ cell that isn't cell itself."""
+ x, y = cell
+ return (
+ len(set(gp.pos[idx][1] for idx, _ in gp.get_points_col(x))) > 1
+ and len(set(gp.pos[idx][0] for idx, _ in gp.get_points_row(y))) > 1
+ )
+
+ def reducible_cells(self, tiling: Tiling) -> Set[Cell]:
+ """Return the set of non-monotone cells with at most one point in a crossing
+ obstrution touching them, and no obstruction touching a cell in the row and
+ a cell in the column."""
+ cells = set(tiling.active_cells) - set(tiling.point_cells)
+ for gp in tiling.obstructions:
+ if not cells:
+ break
+ if not gp.is_localized():
+ seen = set()
+ for cell in gp.pos:
+ if cell in seen or self.gp_in_row_and_col(gp, cell):
+ cells.discard(cell)
+ seen.add(cell)
+ elif len(gp) == 2:
+ cells.discard(gp.pos[0])
+ return cells
+
+ def __repr__(self) -> str:
+ return self.__class__.__name__ + f"({self.tracked})"
+
+ def __str__(self) -> str:
+ return ("tracked " if self.tracked else "") + "cell reduction factory"
+
+ def to_jsonable(self) -> dict:
+ d: dict = super().to_jsonable()
+ d["tracked"] = self.tracked
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "CellReductionFactory":
+ return cls(d["tracked"])
diff --git a/tilings/strategies/deflation.py b/tilings/strategies/deflation.py
new file mode 100644
index 00000000..4a8a2f6a
--- /dev/null
+++ b/tilings/strategies/deflation.py
@@ -0,0 +1,333 @@
+"""The deflation strategy."""
+from typing import Callable, Dict, Iterator, List, Optional, Tuple, cast
+
+import sympy
+
+from comb_spec_searcher import Constructor, Strategy, StrategyFactory
+from comb_spec_searcher.exception import StrategyDoesNotApply
+from comb_spec_searcher.typing import (
+ Parameters,
+ RelianceProfile,
+ SubObjects,
+ SubRecs,
+ SubSamplers,
+ SubTerms,
+ Terms,
+)
+from permuta import Perm
+from tilings import GriddedPerm, Tiling
+from tilings.assumptions import ComponentAssumption, TrackingAssumption
+
+Cell = Tuple[int, int]
+
+
+class DeflationConstructor(Constructor):
+ def __init__(self, parameter: str):
+ self.parameter = parameter
+
+ def get_equation(
+ self, lhs_func: sympy.Function, rhs_funcs: Tuple[sympy.Function, ...]
+ ) -> sympy.Eq:
+ raise NotImplementedError
+
+ def reliance_profile(self, n: int, **parameters: int) -> RelianceProfile:
+ raise NotImplementedError
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ raise NotImplementedError
+
+ def get_sub_objects(
+ self, subobjs: SubObjects, n: int
+ ) -> Iterator[Tuple[Parameters, Tuple[List[Optional[GriddedPerm]]]]]:
+ raise NotImplementedError
+
+ def random_sample_sub_objects(
+ self,
+ parent_count: int,
+ subsamplers: SubSamplers,
+ subrecs: SubRecs,
+ n: int,
+ **parameters: int,
+ ) -> Tuple[GriddedPerm]:
+ raise NotImplementedError
+
+ def equiv(
+ self, other: "Constructor", data: Optional[object] = None
+ ) -> Tuple[bool, Optional[object]]:
+ raise NotImplementedError
+
+
+class DeflationStrategy(Strategy[Tiling, GriddedPerm]):
+ def __init__(
+ self,
+ cell: Cell,
+ sum_deflate: bool,
+ tracked: bool = True,
+ ):
+ self.cell = cell
+ self.tracked = tracked
+ self.sum_deflate = sum_deflate
+ super().__init__()
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return False
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return False
+
+ def shifts(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[int, ...]:
+ return (0, 0)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, Tiling]:
+ deflated_tiling = self.deflated_tiling(comb_class)
+ local_basis = comb_class.sub_tiling([self.cell])
+ if self.tracked:
+ return (
+ deflated_tiling.add_assumption(
+ TrackingAssumption.from_cells([self.cell])
+ ),
+ local_basis,
+ )
+ return deflated_tiling, local_basis
+
+ def deflated_tiling(self, tiling: Tiling) -> Tiling:
+ if self.sum_deflate:
+ extra = Perm((1, 0))
+ else:
+ extra = Perm((0, 1))
+ reduced_reqs = tuple(
+ req
+ for req in tiling.requirements
+ if not all(gp.pos[0] == self.cell and gp.is_single_cell() for gp in req)
+ )
+ reduced_ass = sorted(
+ ass
+ for ass in tiling.assumptions
+ if not isinstance(ass, ComponentAssumption)
+ or GriddedPerm.point_perm(self.cell) not in ass.gps
+ )
+ return Tiling(
+ tiling.obstructions,
+ reduced_reqs,
+ reduced_ass,
+ remove_empty_rows_and_cols=False,
+ derive_empty=False,
+ simplify=False,
+ sorted_input=True,
+ ).add_obstruction(extra, (self.cell, self.cell))
+
+ def constructor(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> DeflationConstructor:
+ if not self.tracked:
+ raise NotImplementedError("The deflation strategy was not tracked")
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Can't deflate the cell")
+ ass = TrackingAssumption.from_cells([self.cell])
+ child_param = children[0].get_assumption_parameter(ass)
+ gp = GriddedPerm.point_perm(self.cell)
+ if any(gp in assumption.gps for assumption in comb_class.assumptions):
+ raise NotImplementedError
+ return DeflationConstructor(child_param)
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Can't deflate the cell")
+ raise NotImplementedError
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str]]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Strategy does not apply")
+ raise NotImplementedError
+
+ def formal_step(self) -> str:
+ return f"deflating cell {self.cell}"
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def to_jsonable(self) -> dict:
+ d: dict = super().to_jsonable()
+ d.pop("ignore_parent")
+ d.pop("inferrable")
+ d.pop("possibly_empty")
+ d.pop("workable")
+ d["cell"] = self.cell
+ d["tracked"] = self.tracked
+ d["sum_deflate"] = self.sum_deflate
+ return d
+
+ def __repr__(self) -> str:
+ args = ", ".join(
+ [
+ f"cell={self.cell!r}",
+ f"sum_deflate={self.sum_deflate!r}",
+ f"tracked={self.tracked!r}",
+ ]
+ )
+ return f"{self.__class__.__name__}({args})"
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "DeflationStrategy":
+ cell = cast(Tuple[int, int], tuple(d.pop("cell")))
+ tracked = d.pop("tracked")
+ sum_deflate = d.pop("sum_deflate")
+ assert not d
+ return cls(cell, sum_deflate, tracked)
+
+ @staticmethod
+ def get_eq_symbol() -> str:
+ return "↣"
+
+
+class DeflationFactory(StrategyFactory[Tiling]):
+ def __init__(self, tracked: bool):
+ self.tracked = tracked
+ super().__init__()
+
+ def __call__(self, comb_class: Tiling) -> Iterator[DeflationStrategy]:
+ for cell in comb_class.active_cells:
+ if not self._can_deflate_requirements_and_assumptions(comb_class, cell):
+ continue
+ if self.can_deflate(comb_class, cell, True):
+ yield DeflationStrategy(cell, True, self.tracked)
+ if self.can_deflate(comb_class, cell, False):
+ yield DeflationStrategy(cell, False, self.tracked)
+
+ @staticmethod
+ def _can_deflate_requirements_and_assumptions(tiling: Tiling, cell: Cell) -> bool:
+ def can_deflate_req_list(req: Tuple[GriddedPerm, ...]) -> bool:
+ return all(gp.pos[0] == cell and gp.is_single_cell() for gp in req) or all(
+ len(list(gp.points_in_cell(cell))) < 2 for gp in req
+ )
+
+ return all(can_deflate_req_list(req) for req in tiling.requirements) and all(
+ not isinstance(ass, ComponentAssumption)
+ or GriddedPerm.point_perm(cell) not in ass.gps
+ or len(ass.gps) == 1
+ for ass in tiling.assumptions
+ )
+
+ @staticmethod
+ def can_deflate(tiling: Tiling, cell: Cell, sum_deflate: bool) -> bool:
+ alone_in_row = tiling.only_cell_in_row(cell)
+ alone_in_col = tiling.only_cell_in_col(cell)
+
+ if alone_in_row and alone_in_col:
+ return False
+
+ deflate_patt = GriddedPerm.single_cell(
+ Perm((1, 0)) if sum_deflate else Perm((0, 1)), cell
+ )
+
+ # we must be sure that no cell in a row or column can interleave
+ # with any reinflated components, so collect cells that do not.
+ cells_not_interleaving = set([cell])
+
+ for ob in tiling.obstructions:
+ if ob == deflate_patt:
+ break # False
+ if ob.is_single_cell() or not ob.occupies(cell):
+ continue
+ number_points_in_cell = sum(1 for c in ob.pos if c == cell)
+ if number_points_in_cell == 1:
+ if len(ob) == 2:
+ # not interleaving with cell as separating if
+ # in same row or column
+ other_cell = [c for c in ob.pos if c != cell][0]
+ cells_not_interleaving.add(other_cell)
+ elif number_points_in_cell == 2:
+ if len(ob) != 3:
+ break # False
+ patt_in_cell = ob.get_gridded_perm_in_cells((cell,))
+ if patt_in_cell != deflate_patt:
+ # you can interleave with components
+ break # False
+ # we need the other cell to be in between the intended deflate
+ # patt in either the row or column
+ other_cell = [c for c in ob.pos if c != cell][0]
+ if ( # in a row or column
+ cell[0] == other_cell[0] or cell[1] == other_cell[1]
+ ) and DeflationFactory.point_in_between(ob, cell, other_cell):
+ # this cell does not interleave with inflated components
+ cells_not_interleaving.add(other_cell)
+ continue
+ break # False
+ elif number_points_in_cell >= 3:
+ # you can interleave with components
+ break # False
+ else:
+ # check that do not interleave with any cells in row or column.
+ return cells_not_interleaving >= tiling.cells_in_row(
+ cell[1]
+ ) and cells_not_interleaving >= tiling.cells_in_col(cell[0])
+ return False
+
+ @staticmethod
+ def point_in_between(ob: GriddedPerm, cell: Cell, other_cell: Cell) -> bool:
+ """Return true if point in other cell is in between point in cell.
+ Assumes a length 3 pattern, and it is in a row or column."""
+ row = cell[1] == other_cell[1]
+ patt = cast(Tuple[int, int, int], ob.patt)
+ if row:
+ left = other_cell[0] < cell[0]
+ if left:
+ return bool(patt[0] == 1)
+ return bool(patt[2] == 1)
+ assert cell[0] == other_cell[0]
+ below = other_cell[1] < cell[1]
+ if below:
+ return bool(patt[1] == 0)
+ return bool(patt[1] == 2)
+
+ def __repr__(self) -> str:
+ return self.__class__.__name__ + f"({self.tracked})"
+
+ def __str__(self) -> str:
+ return ("tracked " if self.tracked else "") + "deflation factory"
+
+ def to_jsonable(self) -> dict:
+ d: dict = super().to_jsonable()
+ d["tracked"] = self.tracked
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "DeflationFactory":
+ return cls(d["tracked"])
diff --git a/tilings/strategies/detect_components.py b/tilings/strategies/detect_components.py
index 10f023ee..4b06d77d 100644
--- a/tilings/strategies/detect_components.py
+++ b/tilings/strategies/detect_components.py
@@ -99,16 +99,13 @@ def equiv(
class DetectComponentsStrategy(Strategy[Tiling, GriddedPerm]):
- @staticmethod
- def can_be_equivalent() -> bool:
+ def can_be_equivalent(self) -> bool:
return False
- @staticmethod
- def is_two_way(comb_class: Tiling):
+ def is_two_way(self, comb_class: Tiling):
return False
- @staticmethod
- def is_reversible(comb_class: Tiling):
+ def is_reversible(self, comb_class: Tiling):
return False
def shifts(
@@ -120,8 +117,7 @@ def shifts(
raise StrategyDoesNotApply
return (0,)
- @staticmethod
- def decomposition_function(comb_class: Tiling) -> Optional[Tuple[Tiling]]:
+ def decomposition_function(self, comb_class: Tiling) -> Optional[Tuple[Tiling]]:
if not comb_class.assumptions:
return None
return (comb_class.remove_components_from_assumptions(),)
@@ -172,12 +168,11 @@ def extra_parameters(
] = child.get_assumption_parameter(mapped_assumption)
return (extra_parameters,)
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "removing exact components"
- @staticmethod
def backward_map(
+ self,
comb_class: Tiling,
objs: Tuple[Optional[GriddedPerm], ...],
children: Optional[Tuple[Tiling, ...]] = None,
@@ -189,8 +184,8 @@ def backward_map(
assert isinstance(objs[0], GriddedPerm)
yield objs[0]
- @staticmethod
def forward_map(
+ self,
comb_class: Tiling,
obj: GriddedPerm,
children: Optional[Tuple[Tiling, ...]] = None,
diff --git a/tilings/strategies/dummy_strategy.py b/tilings/strategies/dummy_strategy.py
new file mode 100644
index 00000000..766fb6f9
--- /dev/null
+++ b/tilings/strategies/dummy_strategy.py
@@ -0,0 +1,86 @@
+from typing import Dict, Iterator, Optional, Tuple
+
+from comb_spec_searcher import Strategy
+from tilings import GriddedPerm, Tiling
+
+from .dummy_constructor import DummyConstructor
+
+
+class DummyStrategy(Strategy[Tiling, GriddedPerm]):
+ def can_be_equivalent(self) -> bool:
+ raise NotImplementedError
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ raise NotImplementedError
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ raise NotImplementedError
+
+ def shifts(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]],
+ ) -> Tuple[int, ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def decomposition_function(
+ self, comb_class: Tiling
+ ) -> Optional[Tuple[Tiling, ...]]:
+ raise NotImplementedError
+
+ def formal_step(self) -> str:
+ return "dummy strategy"
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DummyConstructor:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DummyConstructor()
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DummyConstructor:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DummyConstructor()
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def extra_parameters(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "DummyStrategy":
+ return cls(**d)
diff --git a/tilings/strategies/experimental_verification.py b/tilings/strategies/experimental_verification.py
index 94f11b5f..b410650b 100644
--- a/tilings/strategies/experimental_verification.py
+++ b/tilings/strategies/experimental_verification.py
@@ -16,6 +16,7 @@
from .abstract import BasisAwareVerificationStrategy
__all__ = [
+ "NoRootCellVerificationStrategy",
"ShortObstructionVerificationStrategy",
"SubclassVerificationFactory",
]
@@ -23,6 +24,27 @@
TileScopeVerificationStrategy = VerificationStrategy[Tiling, GriddedPerm]
+class NoRootCellVerificationStrategy(BasisAwareVerificationStrategy):
+ """
+ A strategy to mark as verified any tiling that does not contain the root
+ basis localized in a cell. Tilings with dimensions 1x1 are ignored.
+ """
+
+ def verified(self, comb_class: Tiling):
+ return comb_class.dimensions != (1, 1) and all(
+ frozenset(obs) not in self.symmetries
+ for obs, _ in comb_class.cell_basis().values()
+ )
+
+ def formal_step(self) -> str:
+ basis = ", ".join(str(p) for p in self.basis)
+ return f"tiling has no Av({basis}) cell"
+
+ def __str__(self) -> str:
+ basis = ", ".join(str(p) for p in self.basis)
+ return f"no Av({basis}) cell verification"
+
+
class ShortObstructionVerificationStrategy(BasisAwareVerificationStrategy):
"""
A strategy to mark as verified any tiling whose crossing obstructions all have
diff --git a/tilings/strategies/factor.py b/tilings/strategies/factor.py
index b069ae9f..f256b85f 100644
--- a/tilings/strategies/factor.py
+++ b/tilings/strategies/factor.py
@@ -1,8 +1,19 @@
from collections import Counter
from functools import reduce
-from itertools import chain
+from itertools import chain, combinations
from operator import mul
-from typing import Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple, cast
+from typing import (
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Set,
+ Tuple,
+ Union,
+ cast,
+)
from sympy import Eq, Function
@@ -30,6 +41,10 @@
from tilings.assumptions import TrackingAssumption
from tilings.exception import InvalidOperationError
from tilings.misc import multinomial, partitions_iterator
+from tilings.strategies.assumption_insertion import (
+ AddAssumptionsConstructor,
+ AddAssumptionsStrategy,
+)
Cell = Tuple[int, int]
@@ -40,6 +55,13 @@
"FactorWithMonotoneInterleavingStrategy",
)
+TempGP = Tuple[
+ Tuple[
+ Tuple[Union[float, int], ...], Tuple[Union[float, int], ...], Tuple[Cell, ...]
+ ],
+ ...,
+]
+
class FactorStrategy(CartesianProductStrategy[Tiling, GriddedPerm]):
def __init__(
@@ -49,7 +71,9 @@ def __init__(
workable: bool = True,
):
self.partition = tuple(sorted(tuple(sorted(p)) for p in partition))
- inferrable = any(interleaving_rows_and_cols(self.partition))
+ inferrable = any(
+ FactorWithInterleavingStrategy.interleaving_rows_and_cols(self.partition)
+ )
super().__init__(
ignore_parent=ignore_parent, workable=workable, inferrable=inferrable
)
@@ -69,8 +93,9 @@ def extra_parameters(
comb_class.extra_parameters, comb_class.assumptions
):
for idx, child in enumerate(children):
- # TODO: consider skew/sum
- new_assumption = child.forward_map.map_assumption(assumption)
+ new_assumption = child.forward_map.map_assumption(assumption).avoiding(
+ child.obstructions
+ )
if new_assumption.gps:
child_var = child.get_assumption_parameter(new_assumption)
extra_parameters[idx][parent_var] = child_var
@@ -156,64 +181,6 @@ def from_dict(cls, d: dict) -> "FactorStrategy":
# interleavings of a factor. They are also used by AddInterleavingAssumptionStrategy.
-def interleaving_rows_and_cols(
- partition: Tuple[Tuple[Cell, ...], ...]
-) -> Tuple[Set[int], Set[int]]:
- """
- Return the set of cols and the set of rows that are being interleaved when
- factoring with partition.
- """
- cols: Set[int] = set()
- rows: Set[int] = set()
- x_seen: Set[int] = set()
- y_seen: Set[int] = set()
- for part in partition:
- cols.update(x for x, _ in part if x in x_seen)
- rows.update(y for _, y in part if y in y_seen)
- x_seen.update(x for x, _ in part)
- y_seen.update(y for _, y in part)
- return cols, rows
-
-
-def assumptions_to_add(
- cells: Tuple[Cell, ...], cols: Set[int], rows: Set[int]
-) -> Tuple[TrackingAssumption, ...]:
- """
- Return the assumptions that should be tracked in the set of cells if we are
- interleaving the given rows and cols.
- """
- col_assumptions = [
- TrackingAssumption(
- [GriddedPerm.point_perm(cell) for cell in cells if x == cell[0]]
- )
- for x in cols
- ]
- row_assumptions = [
- TrackingAssumption(
- [GriddedPerm.point_perm(cell) for cell in cells if y == cell[1]]
- )
- for y in rows
- ]
- return tuple(ass for ass in chain(col_assumptions, row_assumptions) if ass.gps)
-
-
-def contains_interleaving_assumptions(
- comb_class: Tiling, partition: Tuple[Tuple[Cell, ...], ...]
-) -> bool:
- """
- Return True if the parent tiling contains all of the necessary tracking
- assumptions needed to count the interleavings, and therefore the
- children too.
- """
- cols, rows = interleaving_rows_and_cols(partition)
- return all(
- ass in comb_class.assumptions
- for ass in chain.from_iterable(
- assumptions_to_add(cells, cols, rows) for cells in partition
- )
- )
-
-
class Interleaving(CartesianProduct[Tiling, GriddedPerm]):
def __init__(
self,
@@ -221,6 +188,7 @@ def __init__(
children: Iterable[Tiling],
extra_parameters: Tuple[Dict[str, str], ...],
interleaving_parameters: Iterable[Tuple[str, ...]],
+ insertion_constructor: Optional[AddAssumptionsConstructor],
):
super().__init__(parent, children, extra_parameters)
self.interleaving_parameters = tuple(interleaving_parameters)
@@ -228,13 +196,13 @@ def __init__(
tuple(parent.extra_parameters.index(k) for k in parameters)
for parameters in interleaving_parameters
)
+ self.insertion_constructor = insertion_constructor
@staticmethod
- def is_equivalence() -> bool:
+ def is_equivalence(is_empty: Optional[Callable[[Tiling], bool]] = None) -> bool:
return False
- @staticmethod
- def get_equation(lhs_func: Function, rhs_funcs: Tuple[Function, ...]) -> Eq:
+ def get_equation(self, lhs_func: Function, rhs_funcs: Tuple[Function, ...]) -> Eq:
raise NotImplementedError
def get_terms(
@@ -253,12 +221,20 @@ def get_terms(
1,
)
interleaved_terms[parameters] += multiplier * value
+ if self.insertion_constructor:
+ new_terms: Terms = Counter()
+ for param, value in interleaved_terms.items():
+ new_terms[self.insertion_constructor.child_param_map(param)] += value
+ return new_terms
return interleaved_terms
def get_sub_objects(
self, subobjs: SubObjects, n: int
) -> Iterator[Tuple[Parameters, Tuple[List[Optional[GriddedPerm]], ...]]]:
- raise NotImplementedError
+ for param, objs in super().get_sub_objects(subobjs, n):
+ if self.insertion_constructor:
+ param = self.insertion_constructor.child_param_map(param)
+ yield param, objs
def random_sample_sub_objects(
self,
@@ -272,32 +248,136 @@ def random_sample_sub_objects(
class FactorWithInterleavingStrategy(FactorStrategy):
+ def __init__(
+ self,
+ partition: Iterable[Iterable[Cell]],
+ ignore_parent: bool = True,
+ workable: bool = True,
+ tracked: bool = True,
+ ):
+ super().__init__(partition, ignore_parent, workable)
+ self.tracked = tracked
+ self.cols, self.rows = self.interleaving_rows_and_cols(self.partition)
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["tracked"] = self.tracked
+ return d
+
+ def __repr__(self) -> str:
+ args = ", ".join(
+ [
+ f"partition={self.partition}",
+ f"ignore_parent={self.ignore_parent}",
+ f"workable={self.workable}",
+ f"tracked={self.tracked}",
+ ]
+ )
+ return f"{self.__class__.__name__}({args})"
+
+ def is_two_way(self, comb_class: Tiling) -> bool: # pylint: disable=W0221
+ return self.is_reversible(comb_class)
+
+ def is_reversible(self, comb_class: Tiling) -> bool: # pylint: disable=W0221
+ return not bool(self.assumptions_to_add(comb_class))
+
def formal_step(self) -> str:
return "interleaving " + super().formal_step()
+ def assumptions_to_add(self, comb_class: Tiling) -> Tuple[TrackingAssumption, ...]:
+ """Return the set of assumptions that need to be added to"""
+ cols, rows = self.interleaving_rows_and_cols(self.partition)
+ return tuple(
+ ass
+ for ass in chain.from_iterable(
+ self._assumptions_to_add(cells, cols, rows) for cells in self.partition
+ )
+ if ass not in comb_class.assumptions
+ )
+
+ @staticmethod
+ def interleaving_rows_and_cols(
+ partition: Tuple[Tuple[Cell, ...], ...]
+ ) -> Tuple[Set[int], Set[int]]:
+ """
+ Return the set of cols and the set of rows that are being interleaved when
+ factoring with partition.
+ """
+ cols: Set[int] = set()
+ rows: Set[int] = set()
+ x_seen: Set[int] = set()
+ y_seen: Set[int] = set()
+ for part in partition:
+ cols.update(x for x, _ in part if x in x_seen)
+ rows.update(y for _, y in part if y in y_seen)
+ x_seen.update(x for x, _ in part)
+ y_seen.update(y for _, y in part)
+ return cols, rows
+
+ @staticmethod
+ def _assumptions_to_add(
+ cells: Tuple[Cell, ...], cols: Set[int], rows: Set[int]
+ ) -> Tuple[TrackingAssumption, ...]:
+ """
+ Return the assumptions that should be tracked in the set of cells if we are
+ interleaving the given rows and cols.
+ """
+ col_assumptions = [
+ TrackingAssumption(
+ [GriddedPerm.point_perm(cell) for cell in cells if x == cell[0]]
+ )
+ for x in cols
+ ]
+ row_assumptions = [
+ TrackingAssumption(
+ [GriddedPerm.point_perm(cell) for cell in cells if y == cell[1]]
+ )
+ for y in rows
+ ]
+ return tuple(ass for ass in chain(col_assumptions, row_assumptions) if ass.gps)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ if self.tracked:
+ comb_class = comb_class.add_assumptions(self.assumptions_to_add(comb_class))
+ return super().decomposition_function(comb_class)
+
def constructor(
self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
) -> Interleaving:
if children is None:
children = self.decomposition_function(comb_class)
- try:
- interleaving_parameters = self.interleaving_parameters(comb_class)
- except ValueError as e:
- # must be untracked
- raise NotImplementedError("The interleaving factor was not tracked.") from e
+ assumptions = self.assumptions_to_add(comb_class)
+ insertion_constructor = None
+ if assumptions:
+ insertion_constructor = AddAssumptionsStrategy(assumptions).constructor(
+ comb_class
+ )
+ comb_class = comb_class.add_assumptions(assumptions)
+ interleaving_parameters = self.interleaving_parameters(comb_class)
+ if interleaving_parameters and not self.tracked:
+ raise NotImplementedError("The interleaving factor was not tracked.")
return Interleaving(
comb_class,
children,
self.extra_parameters(comb_class, children),
interleaving_parameters,
+ insertion_constructor,
)
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ):
+ raise NotImplementedError
+
def interleaving_parameters(self, comb_class: Tiling) -> List[Tuple[str, ...]]:
"""
Return the parameters on the parent tiling that needed to be interleaved.
"""
res: List[Tuple[str, ...]] = []
- cols, rows = interleaving_rows_and_cols(self.partition)
+ cols, rows = self.interleaving_rows_and_cols(self.partition)
for x in cols:
assumptions = [
TrackingAssumption(
@@ -334,15 +414,129 @@ def backward_map(
objs: Tuple[Optional[GriddedPerm], ...],
children: Optional[Tuple[Tiling, ...]] = None,
) -> Iterator[GriddedPerm]:
- raise NotImplementedError
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ gps_to_combine = tuple(
+ tiling.backward_map.map_gp(cast(GriddedPerm, gp))
+ for gp, tiling in zip(objs, children)
+ )
+ all_gps_to_combine: List[TempGP] = [
+ tuple(
+ (tuple(range(len(gp))), tuple(gp.patt), gp.pos) for gp in gps_to_combine
+ )
+ ]
+ for row in self.rows:
+ all_gps_to_combine = self._interleave_row(all_gps_to_combine, row)
+ for col in self.cols:
+ all_gps_to_combine = self._interleave_col(all_gps_to_combine, col)
+
+ for interleaved_gps_to_combine in all_gps_to_combine:
+ temp = [
+ ((cell[0], idx), (cell[1], val))
+ for gp in interleaved_gps_to_combine
+ for idx, val, cell in zip(*gp)
+ ]
+ temp.sort()
+ new_pos = [(idx[0], val[0]) for idx, val in temp]
+ new_patt = Perm.to_standard(val for _, val in temp)
+ assert not GriddedPerm(new_patt, new_pos).contradictory()
+ yield GriddedPerm(new_patt, new_pos)
- def forward_map(
+ def _interleave_row(
self,
- comb_class: Tiling,
- obj: GriddedPerm,
- children: Optional[Tuple[Tiling, ...]] = None,
- ) -> Tuple[GriddedPerm, ...]:
- raise NotImplementedError
+ all_gps_to_combine: List[TempGP],
+ row: int,
+ ) -> List[TempGP]:
+ # pylint: disable=too-many-locals
+ res: List[TempGP] = []
+ for gps_to_combine in all_gps_to_combine:
+ row_points = tuple(
+ tuple(
+ (idx, values[idx])
+ for idx, cell in enumerate(position)
+ if cell[1] == row
+ )
+ for _, values, position in gps_to_combine
+ )
+ total = sum(len(points) for points in row_points)
+ if total == 0:
+ res.append(gps_to_combine)
+ continue
+ min_val = min(val for _, val in chain(*row_points))
+ max_val = max(val for _, val in chain(*row_points)) + 1
+ temp_values = tuple(
+ min_val + i * (max_val - min_val) / total for i in range(total)
+ )
+ for partition in self._partitions(
+ set(temp_values), tuple(len(indices) for indices in row_points)
+ ):
+ new_gps_to_combine = []
+ for part, (indices, values, position), points in zip(
+ partition, gps_to_combine, row_points
+ ):
+ new_values = list(values)
+ actual_indices = [
+ idx for _, idx in sorted((val, idx) for idx, val in points)
+ ]
+ for idx, val in zip(actual_indices, sorted(part)):
+ new_values[idx] = val
+ new_gps_to_combine.append((indices, tuple(new_values), position))
+ res.append(tuple(new_gps_to_combine))
+ return res
+
+ def _interleave_col(
+ self,
+ all_gps_to_combine: List[TempGP],
+ col: int,
+ ):
+ # pylint: disable=too-many-locals
+ res: List[TempGP] = []
+ for gps_to_combine in all_gps_to_combine:
+ col_points = tuple(
+ tuple(
+ (idx, indices[idx])
+ for idx, cell in enumerate(position)
+ if cell[0] == col
+ )
+ for indices, _, position in gps_to_combine
+ )
+ total = sum(len(points) for points in col_points)
+ if total == 0:
+ res.append(gps_to_combine)
+ continue
+ mindex = min(val for _, val in chain(*col_points))
+ maxdex = max(val for _, val in chain(*col_points)) + 1
+ temp_indices = tuple(
+ mindex + i * (maxdex - mindex) / total for i in range(total)
+ )
+ for partition in self._partitions(
+ set(temp_indices), tuple(len(indices) for indices in col_points)
+ ):
+ new_gps_to_combine = []
+ for part, (indices, values, position), points in zip(
+ partition, gps_to_combine, col_points
+ ):
+ new_indices = list(indices)
+ for idx, new_idx in zip([idx for idx, _ in points], sorted(part)):
+ new_indices[idx] = new_idx
+ new_gps_to_combine.append((tuple(new_indices), values, position))
+ res.append(tuple(new_gps_to_combine))
+ return res
+
+ @staticmethod
+ def _partitions(
+ values: Set[float], size_of_parts: Tuple[int, ...]
+ ) -> Iterator[Tuple[Tuple[float, ...], ...]]:
+ if not size_of_parts:
+ if not values:
+ yield tuple()
+ return
+ size = size_of_parts[0]
+ for part in combinations(values, size):
+ for rest in FactorWithInterleavingStrategy._partitions(
+ values - set(part), size_of_parts[1:]
+ ):
+ yield (part,) + rest
@staticmethod
def get_eq_symbol() -> str:
@@ -353,29 +547,8 @@ def get_op_symbol() -> str:
return "*"
-class MonotoneInterleaving(Interleaving):
- pass
-
-
class FactorWithMonotoneInterleavingStrategy(FactorWithInterleavingStrategy):
- def constructor(
- self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
- ) -> MonotoneInterleaving:
- if children is None:
- children = self.decomposition_function(comb_class)
- try:
- interleaving_parameters = self.interleaving_parameters(comb_class)
- except ValueError as e:
- # must be untracked
- raise NotImplementedError(
- "The monotone interleaving was not tracked."
- ) from e
- return MonotoneInterleaving(
- comb_class,
- children,
- self.extra_parameters(comb_class, children),
- interleaving_parameters,
- )
+ pass
class FactorFactory(StrategyFactory[Tiling]):
@@ -424,14 +597,8 @@ def __call__(self, comb_class: Tiling) -> Iterator[FactorStrategy]:
components = tuple(
tuple(chain.from_iterable(part)) for part in partition
)
- if not self.tracked or contains_interleaving_assumptions(
- comb_class, components
- ):
- yield self._build_strategy(components, workable=False)
- if not self.tracked or contains_interleaving_assumptions(
- comb_class, min_comp
- ):
- yield self._build_strategy(min_comp, workable=self.workable)
+ yield self._build_strategy(components, workable=False)
+ yield self._build_strategy(min_comp, workable=self.workable)
def _build_strategy(
self, components: Tuple[Tuple[Cell, ...], ...], workable: bool
@@ -441,9 +608,21 @@ def _build_strategy(
It ensure that a plain factor rule is returned.
"""
- interleaving = any(interleaving_rows_and_cols(components))
- factor_strat = self.factor_class if interleaving else FactorStrategy
- return factor_strat(
+ interleaving = any(
+ FactorWithInterleavingStrategy.interleaving_rows_and_cols(components)
+ )
+ if interleaving:
+ # pylint: disable=E1123
+ return cast(
+ FactorWithInterleavingStrategy,
+ self.factor_class(
+ components,
+ ignore_parent=self.ignore_parent,
+ workable=workable,
+ tracked=self.tracked,
+ ),
+ )
+ return FactorStrategy(
components, ignore_parent=self.ignore_parent, workable=workable
)
diff --git a/tilings/strategies/fusion/component.py b/tilings/strategies/fusion/component.py
index d30e8dc0..afb76fe3 100644
--- a/tilings/strategies/fusion/component.py
+++ b/tilings/strategies/fusion/component.py
@@ -1,15 +1,16 @@
from typing import Iterator, Optional, Tuple
-from comb_spec_searcher import StrategyFactory
+from comb_spec_searcher import Constructor, StrategyFactory
from comb_spec_searcher.strategies import Rule
from tilings import GriddedPerm, Tiling
-from tilings.algorithms import ComponentFusion, Fusion
+from tilings.algorithms import ComponentFusion
+from .constructor import FusionConstructor
from .fusion import FusionStrategy
class ComponentFusionStrategy(FusionStrategy):
- def fusion_algorithm(self, tiling: Tiling) -> Fusion:
+ def fusion_algorithm(self, tiling: Tiling) -> ComponentFusion:
return ComponentFusion(
tiling, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked
)
@@ -32,6 +33,31 @@ def backward_map(
"""
raise NotImplementedError
+ def is_positive_or_empty_fusion(self, tiling: Tiling) -> bool:
+ algo = self.fusion_algorithm(tiling)
+ return sum(1 for ob in tiling.obstructions if algo.is_crossing_len2(ob)) > 1
+
+ def constructor(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> FusionConstructor:
+ if self.tracked and self.is_positive_or_empty_fusion(comb_class):
+ raise NotImplementedError(
+ "Can't count positive or empty fusion. Try a cell insertion!"
+ )
+ return super().constructor(comb_class, children)
+
+ def reverse_constructor( # pylint: disable=no-self-use
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ if self.tracked and self.is_positive_or_empty_fusion(comb_class):
+ raise NotImplementedError(
+ "Can't count positive or empty fusion. Try a cell insertion!"
+ )
+ return super().reverse_constructor(idx, comb_class, children)
+
class ComponentFusionFactory(StrategyFactory[Tiling]):
def __init__(self, tracked: bool = False, isolation_level: Optional[str] = None):
diff --git a/tilings/strategies/fusion/constructor.py b/tilings/strategies/fusion/constructor.py
index 6e9d0592..5cf811de 100644
--- a/tilings/strategies/fusion/constructor.py
+++ b/tilings/strategies/fusion/constructor.py
@@ -17,7 +17,7 @@
from collections import Counter, defaultdict
from functools import reduce
from operator import mul
-from typing import Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
+from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
import sympy
@@ -176,7 +176,7 @@ def get_equation(
"left or right containing more than one point"
)
rhs_func = rhs_funcs[0]
- subs: Dict[str, sympy.Expr] = {
+ subs: Dict[str, Any] = {
child: reduce(mul, [sympy.var(k) for k in parent_vars], 1)
for child, parent_vars in self.reversed_extra_parameters.items()
}
@@ -711,7 +711,11 @@ def __init__(
extra_parameters: Dict[str, str],
left_sided_parameters: Tuple[str, ...],
right_sided_parameters: Tuple[str, ...],
+ left_points: int,
+ right_points: int,
):
+ self.left_points = left_points
+ self.right_points = right_points
left_fuse_index = self.get_left_fuse_index(
left_sided_parameters, fuse_parameter, extra_parameters, t_unfuse
)
@@ -818,6 +822,18 @@ def forward_map(self, param: Parameters) -> Parameters:
for pvalue, fuse_idxs in zip(param, self.unfuse_pos_to_fuse_pos):
for fuse_idx in fuse_idxs:
new_param[fuse_idx] += pvalue
+ if (
+ self.type == ReverseFusionConstructor.Type.LEFT_ONLY
+ and self.right_points
+ and fuse_idx in self.left_sided_index
+ ):
+ new_param[fuse_idx] += 1
+ elif (
+ self.type == ReverseFusionConstructor.Type.RIGHT_ONLY
+ and self.left_points
+ and fuse_idx in self.right_sided_index
+ ):
+ new_param[fuse_idx] += 1
return tuple(new_param)
def a_map(self, param: Parameters) -> Parameters:
diff --git a/tilings/strategies/fusion/fusion.py b/tilings/strategies/fusion/fusion.py
index 3f9837ac..18644cfd 100644
--- a/tilings/strategies/fusion/fusion.py
+++ b/tilings/strategies/fusion/fusion.py
@@ -1,7 +1,7 @@
from collections import defaultdict
-from itertools import islice
+from itertools import chain, islice
from random import randint
-from typing import Dict, Iterator, List, Optional, Set, Tuple, cast
+from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple, cast
from comb_spec_searcher import Constructor, Strategy, StrategyFactory
from comb_spec_searcher.exception import StrategyDoesNotApply
@@ -10,6 +10,7 @@
from tilings import GriddedPerm, Tiling
from tilings.algorithms import Fusion
+from ..pointing import DivideByK
from .constructor import FusionConstructor, ReverseFusionConstructor
@@ -28,8 +29,9 @@ def strategy(self) -> "FusionStrategy":
def constructor(self) -> FusionConstructor:
return cast(FusionConstructor, super().constructor)
- @staticmethod
- def is_equivalence() -> bool:
+ def is_equivalence(
+ self, is_empty: Optional[Callable[[Tiling], bool]] = None
+ ) -> bool:
return False
def _ensure_level_objects(self, n: int) -> None:
@@ -109,6 +111,7 @@ def random_sample_object_of_size(self, n: int, **parameters: int) -> GriddedPerm
)
except StopIteration:
assert 0, "something went wrong"
+ raise RuntimeError("The for-loop for randomly sampling objects was empty")
def _forward_order(
self,
@@ -144,7 +147,7 @@ def __init__(self, row_idx=None, col_idx=None, tracked: bool = False):
def __call__(
self,
comb_class: Tiling,
- children: Tuple[Tiling, ...] = None,
+ children: Optional[Tuple[Tiling, ...]] = None,
) -> FusionRule:
if children is None:
children = self.decomposition_function(comb_class)
@@ -157,17 +160,16 @@ def fusion_algorithm(self, tiling: Tiling) -> Fusion:
tiling, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked
)
- def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
algo = self.fusion_algorithm(comb_class)
if algo.fusable():
return (algo.fused_tiling(),)
+ raise AttributeError("Trying to fuse a tiling that does not fuse")
- @staticmethod
- def can_be_equivalent() -> bool:
+ def can_be_equivalent(self) -> bool:
return False
- @staticmethod
- def is_two_way(comb_class: Tiling):
+ def is_two_way(self, comb_class: Tiling):
return False
def is_reversible(self, comb_class: Tiling) -> bool:
@@ -179,9 +181,8 @@ def is_reversible(self, comb_class: Tiling) -> bool:
)
return new_ass in fused_assumptions
- @staticmethod
def shifts(
- comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
) -> Tuple[int, ...]:
return (0,)
@@ -217,14 +218,12 @@ def reverse_constructor( # pylint: disable=no-self-use
if not self.tracked:
# constructor only enumerates when tracked.
raise NotImplementedError("The fusion strategy was not tracked.")
+ if children is None:
+ children = self.decomposition_function(comb_class)
# Need to recompute some info to count, so ignoring passed in children
algo = self.fusion_algorithm(comb_class)
if not algo.fusable():
raise StrategyDoesNotApply("Strategy does not apply")
- if algo.min_left_right_points() != (0, 0):
- raise NotImplementedError(
- "Reverse positive fusion counting not implemented"
- )
child = algo.fused_tiling()
assert children is None or children == (child,)
(
@@ -232,6 +231,26 @@ def reverse_constructor( # pylint: disable=no-self-use
right_sided_params,
_,
) = self.left_right_both_sided_parameters(comb_class)
+ if not left_sided_params and not right_sided_params:
+ if algo.min_left_right_points() != (0, 0):
+ raise NotImplementedError(
+ "Reverse positive fusion counting not implemented"
+ )
+ fused_assumption = algo.new_assumption()
+ unfused_assumption = fused_assumption.__class__(
+ chain.from_iterable(
+ algo.unfuse_gridded_perm(gp) for gp in fused_assumption.gps
+ )
+ )
+ assert unfused_assumption in comb_class.assumptions
+ return DivideByK(
+ comb_class,
+ children,
+ 1,
+ comb_class.get_assumption_parameter(unfused_assumption),
+ self.extra_parameters(comb_class, children),
+ )
+ left_points, right_points = algo.min_left_right_points()
return ReverseFusionConstructor(
comb_class,
child,
@@ -239,6 +258,8 @@ def reverse_constructor( # pylint: disable=no-self-use
self.extra_parameters(comb_class, children)[0],
tuple(left_sided_params),
tuple(right_sided_params),
+ left_points,
+ right_points,
)
def extra_parameters(
@@ -303,7 +324,7 @@ def backward_map(
comb_class: Tiling,
objs: Tuple[Optional[GriddedPerm], ...],
children: Optional[Tuple[Tiling, ...]] = None,
- left_points: int = None,
+ left_points: Optional[int] = None,
) -> Iterator[GriddedPerm]:
"""
The backward direction of the underlying bijection used for object
diff --git a/tilings/strategies/monotone_sliding.py b/tilings/strategies/monotone_sliding.py
new file mode 100644
index 00000000..e40d2651
--- /dev/null
+++ b/tilings/strategies/monotone_sliding.py
@@ -0,0 +1,234 @@
+from itertools import chain
+from typing import Dict, Iterator, List, Optional, Tuple
+
+from comb_spec_searcher import DisjointUnionStrategy, StrategyFactory
+from comb_spec_searcher.exception import StrategyDoesNotApply
+from comb_spec_searcher.strategies import Rule
+from comb_spec_searcher.strategies.rule import EquivalencePathRule
+from permuta import Perm
+from tilings import GriddedPerm, Tiling
+from tilings.algorithms import Fusion
+
+from .symmetry import TilingRotate90, TilingRotate270
+
+
+class GeneralizedSlidingStrategy(DisjointUnionStrategy[Tiling, GriddedPerm]):
+ """
+ A strategy that slides column idx and idx + 1.
+ """
+
+ def __init__(self, idx: int, rotate: bool = False):
+ super().__init__()
+ self.idx = idx
+ self.rotate = rotate
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ if self.can_slide(comb_class):
+ return (self.slide_tiling(comb_class),)
+ raise StrategyDoesNotApply(f"Sliding idx {self.idx} does not apply")
+
+ def can_slide(self, comb_class: Tiling) -> bool:
+ return MonotoneSlidingFactory.can_slide_col(
+ self.idx, comb_class.rotate270() if self.rotate else comb_class
+ )
+
+ def slide_tiling(self, comb_class: Tiling) -> Tiling:
+ if self.rotate:
+ comb_class = comb_class.rotate270()
+ child = Tiling(
+ self.slide_gps(comb_class.obstructions),
+ map(self.slide_gps, comb_class.requirements),
+ [ass.__class__(self.slide_gps(ass.gps)) for ass in comb_class.assumptions],
+ )
+ if self.rotate:
+ child = child.rotate90()
+ return child
+
+ def formal_step(self) -> str:
+ return f"Sliding index {self.idx}"
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], Optional[GriddedPerm]]:
+ raise NotImplementedError
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ if not comb_class.extra_parameters:
+ return super().extra_parameters(comb_class, children)
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Strategy does not apply")
+ if self.rotate:
+ rules: List[Rule[Tiling, GriddedPerm]] = []
+ parent = comb_class
+ for strategy in [
+ TilingRotate270(),
+ GeneralizedSlidingStrategy(self.idx, False),
+ TilingRotate90(),
+ ]:
+ rules.append(strategy(parent))
+ parent = rules[-1].children[0]
+ return EquivalencePathRule(rules).constructor.extra_parameters
+ child = children[0]
+ return (
+ {
+ comb_class.get_assumption_parameter(
+ ass
+ ): child.get_assumption_parameter(
+ ass.__class__(self.slide_gps(ass.gps))
+ )
+ for ass in comb_class.assumptions
+ },
+ )
+
+ def slide_gp(self, gp: GriddedPerm) -> GriddedPerm:
+ pos = sorted(
+ x if x < self.idx or x > self.idx + 1 else x + 1 if x == self.idx else x - 1
+ for x, _ in gp.pos
+ )
+ return GriddedPerm(gp.patt, ((x, 0) for x in pos))
+
+ def slide_gps(self, gps: Tuple[GriddedPerm, ...]) -> Tuple[GriddedPerm, ...]:
+ return tuple(map(self.slide_gp, gps))
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(idx={self.idx}, rotate={self.rotate})"
+
+ def __str__(self) -> str:
+ return f"slide column {self.idx} and {self.idx + 1}"
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["idx"] = self.idx
+ d["rotate"] = self.rotate
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict):
+ return cls(d["idx"], d["rotate"])
+
+
+class MonotoneSlidingFactory(StrategyFactory[Tiling]):
+ """
+ A factory that creates rules that swaps neighbouring cells if they
+ are 'monotone' fusable, i.e., they are a generalized fusion with
+ a monotone local extra obstruction.
+
+ This is only looks at n x 1 and 1 x n tilings.
+ """
+
+ def __call__(self, comb_class: Tiling) -> Iterator[Rule]:
+ parent = comb_class
+ rotate = False
+ if (
+ not comb_class.dimensions[1] == 1
+ and comb_class.dimensions[0] == 1
+ and not comb_class.requirements
+ ):
+ comb_class = comb_class.rotate270()
+ rotate = True
+
+ if comb_class.dimensions[1] == 1 and not comb_class.requirements:
+ # TODO: allow requirements outside of sliding region
+ for col in range(comb_class.dimensions[0] - 1):
+ if self.can_slide_col(col, comb_class):
+ strategy = GeneralizedSlidingStrategy(col, rotate)
+ child = strategy.slide_tiling(parent)
+ yield strategy(parent, (child,))
+
+ @staticmethod
+ def can_slide_col(col: int, comb_class: Tiling) -> bool:
+ """
+ Return True if the column can be slid.
+ """
+ local_cells = (
+ comb_class.cell_basis()[(col, 0)][0],
+ comb_class.cell_basis()[(col + 1, 0)][0],
+ )
+ if MonotoneSlidingFactory.valid_monotone_sliding_region(
+ col, local_cells, comb_class
+ ):
+ # Check the fusability condition
+ shortest = (
+ col if len(local_cells[0][0]) <= len(local_cells[1][0]) else col + 1
+ )
+ algo = Fusion(comb_class, col_idx=col)
+ fused_obs = tuple(
+ algo.fuse_gridded_perm(gp)
+ for gp in comb_class.obstructions
+ if not all(x == shortest for x, _ in gp.pos)
+ )
+ unfused_obs = tuple(
+ chain.from_iterable(algo.unfuse_gridded_perm(gp) for gp in fused_obs)
+ )
+ return comb_class == comb_class.add_obstructions(unfused_obs)
+ return False
+
+ @staticmethod
+ def valid_monotone_sliding_region(
+ col: int, local_cells: Tuple[List[Perm], List[Perm]], comb_class: Tiling
+ ) -> bool:
+ """
+ Return True if the region is a possible valid monotone sliding region.
+
+ That is:
+ - neighbouring cells are both increasing or decreasing.
+ - the values of non-local obstructions in sliding region are
+ monotone and consecutive in value.
+ """
+
+ def consecutive_value(col: int, tiling: Tiling, incr: bool = True) -> bool:
+ """
+ Return True if the values in the column are consecutive,
+ and increasing or decreasing if incr is True or False.
+ """
+ for gp in tiling.obstructions:
+ if any(x not in (col, col + 1) for x, _ in gp.pos):
+ points = chain(gp.get_points_col(col), gp.get_points_col(col + 1))
+ values = [y for _, y in points]
+ if incr and not all(x + 1 == y for x, y in zip(values, values[1:])):
+ return False
+ if not incr and not all(
+ x - 1 == y for x, y in zip(values, values[1:])
+ ):
+ return False
+ return True
+
+ return (len(local_cells[0]) == 1 and len(local_cells[1]) == 1) and (
+ ( # both cells are increasing, and consecutive values are increasing
+ local_cells[0][0].is_increasing()
+ and local_cells[1][0].is_increasing()
+ and consecutive_value(col, comb_class)
+ )
+ or ( # both cells are decreasing, and consecutive values are decreasing
+ (
+ local_cells[0][0].is_decreasing()
+ and local_cells[1][0].is_decreasing()
+ and consecutive_value(col, comb_class, False)
+ )
+ )
+ )
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}()"
+
+ def __str__(self):
+ return "monotone sliding"
+
+ @classmethod
+ def from_dict(cls, d: dict):
+ return cls()
diff --git a/tilings/strategies/obstruction_inferral.py b/tilings/strategies/obstruction_inferral.py
index 27b24707..26d39e79 100644
--- a/tilings/strategies/obstruction_inferral.py
+++ b/tilings/strategies/obstruction_inferral.py
@@ -109,7 +109,7 @@ class ObstructionInferralFactory(StrategyFactory[Tiling]):
recompute new_obs which is needed for the strategy.
"""
- def __init__(self, maxlen: int = 3):
+ def __init__(self, maxlen: Optional[int] = 3):
self.maxlen = maxlen
super().__init__()
@@ -117,7 +117,9 @@ def new_obs(self, tiling: Tiling) -> Sequence[GriddedPerm]:
"""
Returns the list of new obstructions that can be added to the tiling.
"""
- return AllObstructionInferral(tiling, self.maxlen).new_obs()
+ return AllObstructionInferral(tiling, self.maxlen).new_obs(
+ yield_non_minimal=True
+ )
def __call__(self, comb_class: Tiling) -> Iterator[ObstructionInferralStrategy]:
gps = self.new_obs(comb_class)
diff --git a/tilings/strategies/point_jumping.py b/tilings/strategies/point_jumping.py
new file mode 100644
index 00000000..8f15c61c
--- /dev/null
+++ b/tilings/strategies/point_jumping.py
@@ -0,0 +1,367 @@
+import abc
+from itertools import chain
+from typing import Dict, Iterator, Optional, Tuple
+
+from comb_spec_searcher import Constructor
+from comb_spec_searcher.exception import StrategyDoesNotApply
+from comb_spec_searcher.strategies import (
+ DisjointUnionStrategy,
+ Strategy,
+ StrategyFactory,
+)
+from tilings import GriddedPerm, Tiling, TrackingAssumption
+from tilings.algorithms import Fusion
+
+Cell = Tuple[int, int]
+
+
+class AssumptionOrPointJumpingStrategy(Strategy[Tiling, GriddedPerm]):
+ """
+ An abstract strategy class which moves requirements or assumptions from a
+ column (or row) to its neighbouring column (or row) if the two columns
+ are fusable.
+ """
+
+ def __init__(self, idx1: int, idx2: int, row: bool):
+ self.idx1 = idx1
+ self.idx2 = idx2
+ self.row = row
+ super().__init__()
+
+ @abc.abstractmethod
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ pass
+
+ def swapped_requirements(
+ self, tiling: Tiling
+ ) -> Tuple[Tuple[GriddedPerm, ...], ...]:
+ return tuple(tuple(map(self._swapped_gp, req)) for req in tiling.requirements)
+
+ def swapped_assumptions(self, tiling: Tiling) -> Tuple[TrackingAssumption, ...]:
+ return tuple(
+ ass.__class__(map(self._swapped_gp, ass.gps)) for ass in tiling.assumptions
+ )
+
+ def _swapped_gp(self, gp: GriddedPerm) -> GriddedPerm:
+ if self._in_both_columns(gp):
+ return gp
+ return GriddedPerm(gp.patt, map(self._swap_cell, gp.pos))
+
+ def _in_both_columns(self, gp: GriddedPerm) -> bool:
+ if self.row:
+ return any(y == self.idx1 for _, y in gp.pos) and any(
+ y == self.idx2 for _, y in gp.pos
+ )
+ return any(x == self.idx1 for x, _ in gp.pos) and any(
+ x == self.idx2 for x, _ in gp.pos
+ )
+
+ def _swap_cell(self, cell: Cell) -> Cell:
+ x, y = cell
+ if self.row:
+ if y == self.idx1:
+ y = self.idx2
+ elif y == self.idx2:
+ y = self.idx1
+ else:
+ if x == self.idx1:
+ x = self.idx2
+ elif x == self.idx2:
+ x = self.idx1
+ return x, y
+
+ def _swap_assumption(self, assumption: TrackingAssumption) -> TrackingAssumption:
+ return assumption.__class__(self._swapped_gp(gp) for gp in assumption.gps)
+
+ @abc.abstractmethod
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ pass
+
+ @abc.abstractmethod
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm]]:
+ pass
+
+ @abc.abstractmethod
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ pass
+
+ @abc.abstractmethod
+ def formal_step(self) -> str:
+ pass
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d.pop("ignore_parent")
+ d["idx1"] = self.idx1
+ d["idx2"] = self.idx2
+ d["row"] = self.row
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "AssumptionOrPointJumpingStrategy":
+ return cls(d.pop("idx1"), d.pop("idx2"), d.pop("row"))
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.idx1}, {self.idx2}, {self.row})"
+
+
+class AssumptionAndPointJumpingStrategy(
+ AssumptionOrPointJumpingStrategy,
+ DisjointUnionStrategy[Tiling, GriddedPerm],
+):
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ return (
+ Tiling(
+ comb_class.obstructions,
+ self.swapped_requirements(comb_class),
+ self.swapped_assumptions(comb_class),
+ simplify=False,
+ derive_empty=False,
+ remove_empty_rows_and_cols=False,
+ ),
+ )
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ raise NotImplementedError("not implemented map for assumption or point jumping")
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm]]:
+ raise NotImplementedError("not implemented map for assumption or point jumping")
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Strategy does not apply")
+ child = children[0]
+ return (
+ {
+ comb_class.get_assumption_parameter(
+ ass
+ ): child.get_assumption_parameter(self._swap_assumption(ass))
+ for ass in comb_class.assumptions
+ },
+ )
+
+ def formal_step(self) -> str:
+ row_or_col = "rows" if self.row else "cols"
+ return (
+ f"swapping reqs and assumptions in {row_or_col} {self.idx1} and {self.idx2}"
+ )
+
+
+class AssumptionJumpingStrategy(AssumptionOrPointJumpingStrategy):
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ return (
+ Tiling(
+ comb_class.obstructions,
+ comb_class.requirements,
+ self.swapped_assumptions(comb_class),
+ simplify=False,
+ derive_empty=False,
+ remove_empty_rows_and_cols=False,
+ ),
+ )
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return True
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return True
+
+ def shifts(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]],
+ ) -> Tuple[int, ...]:
+ return (0,)
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ raise NotImplementedError(
+ "Constructor for assumption jumping is not implemented"
+ )
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ raise NotImplementedError(
+ "Reverse constructor for assumption jumping is not implemented."
+ )
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ raise NotImplementedError("not implemented map for assumption or point jumping")
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm]]:
+ raise NotImplementedError("not implemented map for assumption or point jumping")
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ raise NotImplementedError(
+ "extra_parameters not implemented for assumption jumping"
+ )
+
+ def formal_step(self) -> str:
+ row_or_col = "rows" if self.row else "cols"
+ return f"swapping assumptions in {row_or_col} {self.idx1} and {self.idx2}"
+
+
+class PointJumpingStrategy(AssumptionOrPointJumpingStrategy):
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ return (
+ Tiling(
+ comb_class.obstructions,
+ self.swapped_requirements(comb_class),
+ comb_class.assumptions,
+ simplify=False,
+ derive_empty=False,
+ remove_empty_rows_and_cols=False,
+ ),
+ )
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return True
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return True
+
+ def shifts(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]],
+ ) -> Tuple[int, ...]:
+ return (0,)
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ raise NotImplementedError("Constructor not implemented for point jumping")
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Constructor:
+ raise NotImplementedError(
+ "Reverse contructor not implemented for point jumping"
+ )
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ raise NotImplementedError("not implemented map for assumption or point jumping")
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm]]:
+ raise NotImplementedError("not implemented map for assumption or point jumping")
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ raise NotImplementedError("not implemented extra_parameters for point jumping")
+
+ def formal_step(self) -> str:
+ row_or_col = "rows" if self.row else "cols"
+ return f"swapping requirements in {row_or_col} {self.idx1} and {self.idx2}"
+
+
+class AssumptionAndPointJumpingFactory(StrategyFactory[Tiling]):
+ """
+ A factory returning the strategies that moves requirements and/or assumptions
+ across the boundary of two fusable columns (or rows).
+ """
+
+ def __call__(
+ self, comb_class: Tiling
+ ) -> Iterator[AssumptionOrPointJumpingStrategy]:
+ cols, rows = comb_class.dimensions
+ gps_to_be_swapped = chain(
+ *comb_class.requirements, *[ass.gps for ass in comb_class.assumptions]
+ )
+ for col in range(cols - 1):
+ if any(x in (col, col + 1) for gp in gps_to_be_swapped for x, _ in gp.pos):
+ algo = Fusion(comb_class, col_idx=col)
+ if algo.fusable():
+ yield AssumptionAndPointJumpingStrategy(col, col + 1, False)
+ yield AssumptionJumpingStrategy(col, col + 1, False)
+ yield PointJumpingStrategy(col, col + 1, False)
+ for row in range(rows - 1):
+ if any(y in (row, row + 1) for gp in gps_to_be_swapped for y, _ in gp.pos):
+ algo = Fusion(comb_class, row_idx=row)
+ if algo.fusable():
+ yield AssumptionAndPointJumpingStrategy(row, row + 1, True)
+ yield AssumptionJumpingStrategy(row, row + 1, True)
+ yield PointJumpingStrategy(row, row + 1, True)
+
+ def __str__(self) -> str:
+ return "assumption and point jumping"
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}()"
+
+ def __eq__(self, other: object) -> bool:
+ return self.__class__ == other.__class__ and self.__dict__ == other.__dict__
+
+ def __hash__(self) -> int:
+ return hash(self.__class__)
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "AssumptionAndPointJumpingFactory":
+ assert not d
+ return cls()
diff --git a/tilings/strategies/pointing.py b/tilings/strategies/pointing.py
new file mode 100644
index 00000000..c9f60a29
--- /dev/null
+++ b/tilings/strategies/pointing.py
@@ -0,0 +1,581 @@
+"""
+The directionless point placement strategy that is counted
+by the 'pointing' constructor.
+"""
+from collections import Counter
+from itertools import product
+from typing import (
+ Callable,
+ Dict,
+ FrozenSet,
+ Iterator,
+ List,
+ Optional,
+ Tuple,
+ Union,
+ cast,
+)
+
+from comb_spec_searcher import Strategy
+from comb_spec_searcher.exception import StrategyDoesNotApply
+from comb_spec_searcher.strategies.constructor.disjoint import DisjointUnion
+from comb_spec_searcher.strategies.rule import Rule
+from comb_spec_searcher.strategies.strategy import StrategyFactory
+from comb_spec_searcher.typing import CombinatorialClassType, SubTerms, Terms
+from permuta.misc import DIR_NONE
+from tilings import GriddedPerm, Tiling
+from tilings.algorithms import RequirementPlacement
+from tilings.assumptions import ComponentAssumption, TrackingAssumption
+from tilings.strategies.assumption_insertion import AddAssumptionsStrategy
+from tilings.strategies.obstruction_inferral import ObstructionInferralStrategy
+from tilings.tiling import Cell
+
+from .unfusion import DivideByN, ReverseDivideByN
+
+
+class PointingStrategy(Strategy[Tiling, GriddedPerm]):
+ def __init__(
+ self,
+ max_cells: Optional[int] = 4,
+ ignore_parent: bool = False,
+ inferrable: bool = True,
+ possibly_empty: bool = True,
+ workable: bool = True,
+ ) -> None:
+ self.max_cells = max_cells
+ super().__init__(ignore_parent, inferrable, possibly_empty, workable)
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return True
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return True
+
+ def shifts(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]],
+ ) -> Tuple[int, ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ assert children is not None
+ return tuple(0 for _ in children)
+
+ @staticmethod
+ def already_placed_cells(comb_class: Tiling) -> FrozenSet[Cell]:
+ return frozenset(
+ cell
+ for cell in comb_class.point_cells
+ if comb_class.only_cell_in_row_and_col(cell)
+ )
+
+ def cells_to_place(self, comb_class: Tiling) -> FrozenSet[Cell]:
+ return comb_class.active_cells - self.already_placed_cells(comb_class)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ assert self.max_cells is not None
+ if len(comb_class.active_cells) <= self.max_cells:
+ cells = self.cells_to_place(comb_class)
+ if cells:
+ return tuple(
+ comb_class.place_point_in_cell(cell, DIR_NONE)
+ for cell in sorted(cells)
+ )
+ raise StrategyDoesNotApply("The tiling is just point cells!")
+ raise StrategyDoesNotApply("Too many active cells.")
+
+ def formal_step(self) -> str:
+ return f"directionless point placement (<= {self.max_cells} cells)"
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DivideByN:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DivideByN(
+ comb_class,
+ children,
+ -len(self.already_placed_cells(comb_class)),
+ self.extra_parameters(comb_class, children),
+ )
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> ReverseDivideByN:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return ReverseDivideByN(
+ comb_class,
+ children,
+ idx,
+ -len(self.already_placed_cells(comb_class)),
+ self.extra_parameters(comb_class, children),
+ )
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def extra_parameters(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ cells = self.cells_to_place(comb_class)
+ algo = RequirementPlacement(comb_class, True, True)
+ res = []
+ for child, cell in zip(children, sorted(cells)):
+ params: Dict[str, str] = {}
+ mapped_assumptions = [
+ child.forward_map.map_assumption(ass).avoiding(child.obstructions)
+ for ass in algo.stretched_assumptions(cell)
+ ]
+ for ass, mapped_ass in zip(comb_class.assumptions, mapped_assumptions):
+ if mapped_ass.gps:
+ params[
+ comb_class.get_assumption_parameter(ass)
+ ] = child.get_assumption_parameter(mapped_ass)
+ res.append(params)
+ return tuple(res)
+
+ def __repr__(self) -> str:
+ return (
+ self.__class__.__name__ + f"({self.max_cells}, {self.ignore_parent}, "
+ f"{self.inferrable}, {self.possibly_empty}, {self.workable})"
+ )
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["max_cells"] = self.max_cells
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "PointingStrategy":
+ return cls(**d)
+
+
+class DivideByK(DivideByN):
+ """
+ A constructor that works as disjoint union
+ but divides the values by k + shift.
+ """
+
+ def __init__(
+ self,
+ parent: CombinatorialClassType,
+ children: Tuple[CombinatorialClassType, ...],
+ shift: int,
+ parameter: str,
+ extra_parameters: Optional[Tuple[Dict[str, str], ...]] = None,
+ ):
+ self.parameter = parameter
+ self.division_index = parent.extra_parameters.index(parameter)
+ super().__init__(parent, children, shift, extra_parameters)
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ if n + self.shift <= 0:
+ return self.initial_conditions[n]
+ terms = DisjointUnion.get_terms(self, parent_terms, subterms, n)
+ return Counter(
+ {
+ key: value // (key[self.division_index] + self.shift)
+ if (key[self.division_index] + self.shift) != 0
+ else value
+ for key, value in terms.items()
+ }
+ )
+
+ def __str__(self):
+ return f"divide by {self.parameter}"
+
+
+class ReverseDivideByK(ReverseDivideByN):
+ """
+ The complement version of DivideByK.
+ It works as Complement, but multiplies by k + shift the original left hand side.
+ """
+
+ def __init__(
+ self,
+ parent: CombinatorialClassType,
+ children: Tuple[CombinatorialClassType, ...],
+ idx: int,
+ shift: int,
+ parameter: str,
+ extra_parameters: Optional[Tuple[Dict[str, str], ...]] = None,
+ ):
+ self.parameter = parameter
+ self.division_index = parent.extra_parameters.index(parameter)
+ super().__init__(parent, children, idx, shift, extra_parameters)
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ if n + self.shift <= 0:
+ return self.initial_conditions[n]
+ parent_terms_mapped: Terms = Counter()
+ for param, value in subterms[0](n).items():
+ if value:
+ # This is the only change from complement
+ K = param[self.division_index] + self.shift
+ assert K >= 0
+ if K == 0:
+ parent_terms_mapped[self._parent_param_map(param)] += value
+ else:
+ parent_terms_mapped[self._parent_param_map(param)] += value * K
+ children_terms = subterms[1:]
+ for child_terms, param_map in zip(children_terms, self._children_param_maps):
+ # we subtract from total
+ for param, value in child_terms(n).items():
+ mapped_param = self._parent_param_map(param_map(param))
+ parent_terms_mapped[mapped_param] -= value
+ assert parent_terms_mapped[mapped_param] >= 0
+ if parent_terms_mapped[mapped_param] == 0:
+ parent_terms_mapped.pop(mapped_param)
+
+ return parent_terms_mapped
+
+ def __str__(self):
+ return f"reverse divide by {self.parameter}"
+
+
+class AssumptionPointingStrategy(PointingStrategy):
+ def __init__(
+ self,
+ assumption: TrackingAssumption,
+ ignore_parent: bool = False,
+ inferrable: bool = True,
+ possibly_empty: bool = True,
+ workable: bool = True,
+ ):
+ self.assumption = assumption
+ assert not isinstance(assumption, ComponentAssumption)
+ self.cells = frozenset(gp.pos[0] for gp in assumption.gps)
+ super().__init__(None, ignore_parent, inferrable, possibly_empty, workable)
+
+ def cells_to_place(self, comb_class: Tiling) -> FrozenSet[Cell]:
+ return super().cells_to_place(comb_class).intersection(self.cells)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ if self.assumption not in comb_class.assumptions:
+ raise StrategyDoesNotApply("The assumption is not on tiling")
+ cells = self.cells_to_place(comb_class)
+ if cells:
+ return (
+ comb_class.add_obstructions(
+ [GriddedPerm.point_perm(cell) for cell in cells]
+ ),
+ ) + tuple(
+ comb_class.place_point_in_cell(cell, DIR_NONE) for cell in sorted(cells)
+ )
+ raise StrategyDoesNotApply("The assumption is just point cells!")
+
+ def formal_step(self) -> str:
+ return super().formal_step() + f" in cells {set(self.cells)}"
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DivideByN:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DivideByK(
+ comb_class,
+ children,
+ -len(self.cells.intersection(self.already_placed_cells(comb_class))),
+ comb_class.get_assumption_parameter(self.assumption),
+ self.extra_parameters(comb_class, children),
+ )
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> ReverseDivideByN:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return ReverseDivideByK(
+ comb_class,
+ children,
+ idx,
+ -len(self.cells.intersection(self.already_placed_cells(comb_class))),
+ comb_class.get_assumption_parameter(self.assumption),
+ self.extra_parameters(comb_class, children),
+ )
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ empty_params = ObstructionInferralStrategy(
+ [GriddedPerm.point_perm(cell) for cell in self.cells_to_place(comb_class)]
+ ).extra_parameters(comb_class)
+ return empty_params + super().extra_parameters(comb_class, children[1:])
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["assumption"] = self.assumption.to_jsonable()
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "AssumptionPointingStrategy":
+ if "max_cells" in d:
+ d.pop("max_cells")
+ assumption = TrackingAssumption.from_dict(d.pop("assumption"))
+ return cls(assumption=assumption, **d)
+
+ def __repr__(self) -> str:
+ return (
+ self.__class__.__name__
+ + f"({repr(self.assumption)}, {self.ignore_parent}, "
+ f"{self.inferrable}, {self.possibly_empty}, {self.workable})"
+ )
+
+
+class AssumptionPointingFactory(StrategyFactory[Tiling]):
+ def __init__(self, max_cells: int = 4) -> None:
+ self.max_cells = max_cells
+ super().__init__()
+
+ def __call__(self, comb_class: Tiling) -> Iterator[AssumptionPointingStrategy]:
+ if len(comb_class.active_cells) <= self.max_cells:
+ for assumption in comb_class.assumptions:
+ if not isinstance(assumption, ComponentAssumption):
+ yield AssumptionPointingStrategy(assumption)
+
+ def __str__(self) -> str:
+ return f"assumption pointing strategy (<= {self.max_cells} cells)"
+
+ def __repr__(self) -> str:
+ return self.__class__.__name__ + f"({self.max_cells})"
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["max_cells"] = self.max_cells
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "AssumptionPointingFactory":
+
+ return cls(**d)
+
+
+class RequirementPointingStrategy(PointingStrategy):
+ def __init__(
+ self,
+ gps: Tuple[GriddedPerm, ...],
+ indices: Tuple[int, ...],
+ ignore_parent: bool = False,
+ inferrable: bool = True,
+ possibly_empty: bool = True,
+ workable: bool = True,
+ ):
+ assert len(gps) == len(indices)
+ self.gps = gps
+ self.indices = indices
+ super().__init__(None, ignore_parent, inferrable, possibly_empty, workable)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ cells = self.cells_to_place(comb_class)
+ algo = RequirementPlacement(comb_class)
+ if cells:
+ return algo.place_point_of_req(
+ self.gps, self.indices, DIR_NONE, include_not=True, cells=cells
+ )
+ raise StrategyDoesNotApply("The assumption is just point cells!")
+
+ def formal_step(self) -> str:
+ return super().formal_step() + f" in {self.gps} at indices {self.indices}"
+
+ def extra_parameters(
+ self: Union[
+ "RequirementPointingStrategy", "RequirementAssumptionPointingStrategy"
+ ],
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ even_index = tuple(child for idx, child in enumerate(children) if idx % 2 == 0)
+ odd_index = tuple(child for idx, child in enumerate(children) if idx % 2 == 1)
+ res: List[Optional[Dict[str, str]]] = [None for _ in children]
+ for idx, param in enumerate(
+ PointingStrategy.extra_parameters(self, comb_class, even_index)
+ ):
+ res[2 * idx] = param
+ for idx, param in enumerate(
+ PointingStrategy.extra_parameters(self, comb_class, odd_index)
+ ):
+ res[2 * idx + 1] = param
+ cast(List[Dict[str, str]], res)
+ return tuple(cast(List[Dict[str, str]], res))
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["gps"] = [gp.to_jsonable() for gp in self.gps]
+ d["indices"] = self.indices
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "RequirementPointingStrategy":
+ if "max_cells" in d:
+ d.pop("max_cells")
+ return cls(
+ tuple(GriddedPerm.from_dict(gp) for gp in d.pop("gps")),
+ tuple(d.pop("indices")),
+ **d,
+ )
+
+ def __repr__(self) -> str:
+ return (
+ self.__class__.__name__
+ + f"({self.gps}, {self.indices}, {self.ignore_parent}, "
+ f"{self.inferrable}, {self.possibly_empty}, {self.workable})"
+ )
+
+
+class RequirementAssumptionPointingStrategy(AssumptionPointingStrategy):
+ def __init__(
+ self,
+ gps: Tuple[GriddedPerm, ...],
+ indices: Tuple[int, ...],
+ ignore_parent: bool = False,
+ inferrable: bool = True,
+ possibly_empty: bool = True,
+ workable: bool = True,
+ ):
+ assert len(gps) == len(indices)
+ self.gps = gps
+ self.indices = indices
+ self.cells = frozenset(gp.pos[idx] for gp, idx in zip(gps, indices))
+ self.assumption = TrackingAssumption.from_cells(self.cells)
+
+ super().__init__(
+ self.assumption, ignore_parent, inferrable, possibly_empty, workable
+ )
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ if self.assumption not in comb_class.assumptions:
+ raise StrategyDoesNotApply("The assumption is not on tiling")
+ cells = self.cells_to_place(comb_class)
+ algo = RequirementPlacement(comb_class)
+ if cells:
+ return (
+ comb_class.add_obstructions(
+ [GriddedPerm.point_perm(cell) for cell in cells]
+ ),
+ ) + algo.place_point_of_req(
+ self.gps, self.indices, DIR_NONE, include_not=True
+ )
+ raise StrategyDoesNotApply("The assumption is just point cells!")
+
+ def formal_step(self) -> str:
+ return super().formal_step() + f" in {self.gps} at indices {self.indices}"
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ empty_params = ObstructionInferralStrategy(
+ [GriddedPerm.point_perm(cell) for cell in self.cells_to_place(comb_class)]
+ ).extra_parameters(comb_class)
+ rest = RequirementPointingStrategy.extra_parameters(
+ self, comb_class, children[1:]
+ )
+ return empty_params + tuple(rest)
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["gps"] = [gp.to_jsonable() for gp in self.gps]
+ d["indices"] = self.indices
+ d.pop("assumption")
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "RequirementAssumptionPointingStrategy":
+ if "max_cells" in d:
+ d.pop("max_cells")
+ return cls(
+ tuple(GriddedPerm.from_dict(gp) for gp in d.pop("gps")),
+ tuple(d.pop("indices")),
+ **d,
+ )
+
+ def __repr__(self) -> str:
+ return (
+ self.__class__.__name__
+ + f"({self.gps}, {self.indices}, {self.ignore_parent}, "
+ f"{self.inferrable}, {self.possibly_empty}, {self.workable})"
+ )
+
+
+class RequirementPointingFactory(StrategyFactory[Tiling]):
+ def __init__(self, max_cells: int = 4) -> None:
+ self.max_cells = max_cells
+ super().__init__()
+
+ def __call__(self, comb_class: Tiling) -> Iterator[Rule]:
+ for gps in comb_class.requirements:
+ for indices in product(*[range(len(gp)) for gp in gps]):
+ untracked_strategy = RequirementPointingStrategy(gps, indices)
+ if len(comb_class.active_cells) <= self.max_cells:
+ yield untracked_strategy(comb_class)
+ strategy = RequirementAssumptionPointingStrategy(gps, indices)
+ cells_to_place = strategy.cells_to_place(comb_class)
+ if (
+ untracked_strategy.cells_to_place(comb_class) != cells_to_place
+ and 0 < len(cells_to_place) <= self.max_cells
+ ):
+ parent = comb_class
+ if strategy.assumption not in comb_class.assumptions:
+ rule = AddAssumptionsStrategy([strategy.assumption])(comb_class)
+ yield rule
+ parent = rule.children[0]
+ yield strategy(parent)
+
+ def __str__(self) -> str:
+ return f"requirement pointing strategy (<= {self.max_cells} cells)"
+
+ def __repr__(self) -> str:
+ return self.__class__.__name__ + f"({self.max_cells})"
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["max_cells"] = self.max_cells
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "RequirementPointingFactory":
+ return cls(**d)
diff --git a/tilings/strategies/rearrange_assumption.py b/tilings/strategies/rearrange_assumption.py
index e1ade858..a8353443 100644
--- a/tilings/strategies/rearrange_assumption.py
+++ b/tilings/strategies/rearrange_assumption.py
@@ -1,11 +1,16 @@
from collections import Counter
from functools import partial
from itertools import combinations
-from typing import Callable, Dict, Iterator, List, Optional, Tuple
+from typing import Callable, Dict, Iterator, List, Optional, Tuple, Union
import sympy
-from comb_spec_searcher import Constructor, Strategy, StrategyFactory
+from comb_spec_searcher import (
+ Constructor,
+ DisjointUnionStrategy,
+ Strategy,
+ StrategyFactory,
+)
from comb_spec_searcher.exception import StrategyDoesNotApply
from comb_spec_searcher.typing import (
Parameters,
@@ -18,7 +23,12 @@
Terms,
)
from tilings import GriddedPerm, Tiling
-from tilings.assumptions import TrackingAssumption
+from tilings.assumptions import (
+ ComponentAssumption,
+ SkewComponentAssumption,
+ SumComponentAssumption,
+ TrackingAssumption,
+)
Cell = Tuple[int, int]
@@ -275,21 +285,17 @@ def __init__(
self.sub_assumption = sub_assumption
super().__init__()
- @staticmethod
- def can_be_equivalent() -> bool:
+ def can_be_equivalent(self) -> bool:
return False
- @staticmethod
- def is_two_way(comb_class: Tiling) -> bool:
+ def is_two_way(self, comb_class: Tiling) -> bool:
return True
- @staticmethod
- def is_reversible(comb_class: Tiling) -> bool:
+ def is_reversible(self, comb_class: Tiling) -> bool:
return True
- @staticmethod
def shifts(
- comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
) -> Tuple[int, ...]:
return (0,)
@@ -364,8 +370,6 @@ def backward_map(
The backward direction of the underlying bijection used for object
generation and sampling.
"""
- if children is None:
- children = self.decomposition_function(comb_class)
assert len(objs) == 1 and objs[0] is not None
yield objs[0]
@@ -379,8 +383,6 @@ def forward_map(
The forward direction of the underlying bijection used for object
generation and sampling.
"""
- if children is None:
- children = self.decomposition_function(comb_class)
return (obj,)
def to_jsonable(self) -> dict:
@@ -414,15 +416,122 @@ def get_eq_symbol() -> str:
return "↣"
+class ComponentToPointAssumptionStrategy(
+ DisjointUnionStrategy[Tiling, GriddedPerm],
+):
+ """A strategy that changes a component tracking assumption to a point
+ tracking assumption."""
+
+ def __init__(
+ self,
+ assumption: TrackingAssumption,
+ ignore_parent: bool = False,
+ workable: bool = False,
+ ):
+ assert isinstance(assumption, ComponentAssumption)
+ self.assumption = assumption
+ self.new_assumption = TrackingAssumption(assumption.gps)
+ super().__init__(
+ ignore_parent=ignore_parent,
+ inferrable=False,
+ possibly_empty=False,
+ workable=workable,
+ )
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ if self.assumption not in comb_class.assumptions:
+ raise StrategyDoesNotApply("Assumption not on tiling")
+ return (
+ comb_class.remove_assumption(self.assumption).add_assumption(
+ self.new_assumption
+ ),
+ )
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ assert len(objs) == 1 and objs[0] is not None
+ yield objs[0]
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm]]:
+ return (obj,)
+
+ def extra_parameters(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[Dict[str, str], ...]:
+ if not comb_class.extra_parameters:
+ return super().extra_parameters(comb_class, children)
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("Strategy does not apply")
+ child = children[0]
+ return (
+ {
+ comb_class.get_assumption_parameter(
+ ass
+ ): child.get_assumption_parameter(
+ self.new_assumption if ass == self.assumption else ass
+ )
+ for ass in comb_class.assumptions
+ },
+ )
+
+ def formal_step(self) -> str:
+ cells = ", ".join(str(gp.pos[0]) for gp in self.assumption.gps)
+ return f"change component assumption in cells {cells} to point assumption"
+
+ def to_jsonable(self) -> dict:
+ d: dict = super().to_jsonable()
+ d["assumption"] = self.assumption.to_jsonable()
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "ComponentToPointAssumptionStrategy":
+ return cls(
+ assumption=TrackingAssumption.from_dict(d["assumption"]),
+ ignore_parent=d["ignore_parent"],
+ workable=d["workable"],
+ )
+
+
class RearrangeAssumptionFactory(StrategyFactory[Tiling]):
- def __call__(self, comb_class: Tiling) -> Iterator[RearrangeAssumptionStrategy]:
- assumptions = comb_class.assumptions
- for ass1, ass2 in combinations(assumptions, 2):
+ def __call__(
+ self, comb_class: Tiling
+ ) -> Iterator[
+ Union[RearrangeAssumptionStrategy, ComponentToPointAssumptionStrategy]
+ ]:
+ points: List[TrackingAssumption] = []
+ components: List[TrackingAssumption] = []
+ for ass in comb_class.assumptions:
+ (points, components)[isinstance(ass, ComponentAssumption)].append(ass)
+
+ for ass1, ass2 in combinations(points, 2):
if set(ass1.gps).issubset(set(ass2.gps)):
yield RearrangeAssumptionStrategy(ass2, ass1)
if set(ass2.gps).issubset(set(ass1.gps)):
yield RearrangeAssumptionStrategy(ass1, ass2)
+ for ass in components:
+ if self.can_be_point_assumption(comb_class, ass):
+ yield ComponentToPointAssumptionStrategy(ass)
+
+ @staticmethod
+ def can_be_point_assumption(tiling: Tiling, assumption: TrackingAssumption) -> bool:
+ sub_tiling = tiling.sub_tiling(tuple(gp.pos[0] for gp in assumption.gps))
+ if isinstance(assumption, SumComponentAssumption):
+ return sub_tiling.is_increasing()
+ assert isinstance(assumption, SkewComponentAssumption)
+ return sub_tiling.is_decreasing()
+
def __repr__(self) -> str:
return self.__class__.__name__ + "()"
diff --git a/tilings/strategies/relax_assumption.py b/tilings/strategies/relax_assumption.py
new file mode 100644
index 00000000..c519c6c0
--- /dev/null
+++ b/tilings/strategies/relax_assumption.py
@@ -0,0 +1,120 @@
+from typing import Dict, Iterator, Optional, Tuple
+
+from comb_spec_searcher import Strategy, StrategyFactory
+from comb_spec_searcher.exception import StrategyDoesNotApply
+from comb_spec_searcher.strategies import Rule
+from tilings import GriddedPerm, Tiling
+from tilings.strategies.dummy_constructor import DummyConstructor
+
+
+class RelaxAssumptionStrategy(Strategy[Tiling, GriddedPerm]):
+ def __init__(self, child: Tiling, assumption_idx: int, **kwargs):
+ self.child = child
+ self.assumption_idx = assumption_idx
+ super().__init__(**kwargs)
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return False
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return False
+
+ def shifts(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]],
+ ) -> Tuple[int, ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return (0,)
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]:
+ try:
+ assumption = self.child.assumptions[self.assumption_idx]
+ except IndexError as e:
+ raise StrategyDoesNotApply from e
+ parent = self.child.add_obstructions(assumption.gps)
+ if parent != comb_class:
+ raise StrategyDoesNotApply
+ return (self.child,)
+
+ def formal_step(self) -> str:
+ return f"the assumption at index {self.assumption_idx} is relaxed"
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DummyConstructor:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DummyConstructor()
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DummyConstructor:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DummyConstructor()
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def extra_parameters(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["child"] = self.child.to_jsonable()
+ d["assumption_idx"] = self.assumption_idx
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "RelaxAssumptionStrategy":
+ return cls(d.pop("child"), d.pop("assumption_idx"), **d)
+
+
+class RelaxAssumptionFactory(StrategyFactory[Tiling]):
+ def __call__(self, comb_class: Tiling) -> Iterator[Rule]:
+ for idx, assumption in enumerate(comb_class.assumptions):
+ parent = comb_class.add_obstructions(assumption.gps)
+ yield RelaxAssumptionStrategy(comb_class, idx)(parent, (comb_class,))
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "RelaxAssumptionFactory":
+ return cls(**d)
+
+ def __str__(self) -> str:
+ return "Relax assumption"
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}()"
diff --git a/tilings/strategies/requirement_insertion.py b/tilings/strategies/requirement_insertion.py
index e3098cbd..4a8fd257 100644
--- a/tilings/strategies/requirement_insertion.py
+++ b/tilings/strategies/requirement_insertion.py
@@ -1,13 +1,17 @@
import abc
from itertools import chain, product
-from typing import Dict, Iterable, Iterator, List, Optional, Tuple, cast
+from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple, cast
+import tilings.strategies as strat
from comb_spec_searcher import DisjointUnionStrategy, StrategyFactory
from comb_spec_searcher.exception import StrategyDoesNotApply
from comb_spec_searcher.strategies import Rule
+from comb_spec_searcher.strategies.strategy import VerificationStrategy
from permuta import Av, Perm
from tilings import GriddedPerm, Tiling
+from tilings.algorithms import Factor, SubobstructionInferral
+Cell = Tuple[int, int]
ListRequirement = Tuple[GriddedPerm, ...]
EXTRA_BASIS_ERR = "'extra_basis' should be a list of Perm to avoid"
@@ -396,8 +400,10 @@ def __init__(
extra_basis: Optional[List[Perm]] = None,
limited_insertion: bool = True,
ignore_parent: bool = False,
+ allow_factorable_insertions: bool = False,
) -> None:
self.limited_insertion = limited_insertion
+ self.allow_factorable_insertions = allow_factorable_insertions
super().__init__(maxreqlen, extra_basis, ignore_parent)
def req_lists_to_insert(self, tiling: Tiling) -> Iterator[ListRequirement]:
@@ -410,7 +416,7 @@ def req_lists_to_insert(self, tiling: Tiling) -> Iterator[ListRequirement]:
)
for length in range(1, self.maxreqlen + 1):
for gp in obs_tiling.gridded_perms_of_length(length):
- if len(gp.factors()) == 1 and all(
+ if (self.allow_factorable_insertions or len(gp.factors()) == 1) and all(
p not in gp.patt for p in self.extra_basis
):
yield (GriddedPerm(gp.patt, gp.pos),)
@@ -436,6 +442,7 @@ def __repr__(self) -> str:
f"extra_basis={self.extra_basis!r}",
f"limited_insertion={self.limited_insertion}",
f"ignore_parent={self.ignore_parent}",
+ f"allow_factorable_insertions={self.allow_factorable_insertions}",
]
)
return f"{self.__class__.__name__}({args})"
@@ -443,6 +450,7 @@ def __repr__(self) -> str:
def to_jsonable(self) -> dict:
d: dict = super().to_jsonable()
d["limited_insertion"] = self.limited_insertion
+ d["allow_factorable_insertions"] = self.allow_factorable_insertions
return d
@classmethod
@@ -454,10 +462,12 @@ def from_dict(cls, d: dict) -> "RequirementInsertionWithRestrictionFactory":
d.pop("extra_basis")
limited_insertion = d.pop("limited_insertion")
maxreqlen = d.pop("maxreqlen")
+ allow_factorable_insertions = d.pop("allow_factorable_insertions", False)
return cls(
maxreqlen=maxreqlen,
extra_basis=extra_basis,
limited_insertion=limited_insertion,
+ allow_factorable_insertions=allow_factorable_insertions,
**d,
)
@@ -514,6 +524,70 @@ def __str__(self) -> str:
return "requirement corroboration"
+class PositiveCorroborationFactory(AbstractRequirementInsertionFactory):
+ """
+ The positive corroboration strategy.
+
+ The positive corroboration strategy inserts points into any two
+ cells which can not both be positive, i.e., one is positive
+ and the other is empty.
+ """
+
+ def __init__(self, ignore_parent: bool = True):
+ super().__init__(ignore_parent)
+
+ @staticmethod
+ def cells_to_yield(tiling: Tiling) -> Set[Cell]:
+ potential_cells: Set[Tuple[Cell, ...]] = set()
+ cells_to_yield: Set[Cell] = set()
+ for gp in tiling.obstructions:
+ if len(gp) == 2 and not gp.is_localized():
+ if gp.is_interleaving():
+ cells = tuple(sorted(gp.pos))
+ if cells in potential_cells:
+ cells_to_yield.update(cells)
+ else:
+ potential_cells.add(cells)
+ else:
+ cells_to_yield.update(gp.pos)
+ return cells_to_yield
+
+ def req_lists_to_insert(self, tiling: Tiling) -> Iterator[ListRequirement]:
+ for cell in self.cells_to_yield(tiling):
+ if cell not in tiling.point_cells:
+ yield (GriddedPerm.point_perm(cell),)
+
+ def __str__(self) -> str:
+ return "positive corroboration"
+
+
+class PointCorroborationFactory(PositiveCorroborationFactory):
+ """
+ The point corroboration strategy.
+
+ The point corroboration strategy inserts points into any two point
+ or empty cells which can not both be a point, i.e., one is a point
+ and the other is empty.
+ """
+
+ @staticmethod
+ def cells_to_yield(tiling: Tiling) -> Set[Cell]:
+ cell_basis = tiling.cell_basis()
+ point_or_empty_cells = set()
+ for cell, (patts, _) in cell_basis.items():
+ if patts == [Perm((0, 1)), Perm((1, 0))]:
+ point_or_empty_cells.add(cell)
+ point_or_empty_cells = point_or_empty_cells - tiling.point_cells
+ if point_or_empty_cells:
+ return point_or_empty_cells.intersection(
+ PositiveCorroborationFactory.cells_to_yield(tiling)
+ )
+ return set()
+
+ def __str__(self) -> str:
+ return "point corroboration"
+
+
class RemoveRequirementFactory(StrategyFactory[Tiling]):
"""
For a tiling T, and each requirement R on T, create the rules that
@@ -534,3 +608,147 @@ def from_dict(cls, d: dict) -> "RemoveRequirementFactory":
def __repr__(self) -> str:
return f"{self.__class__.__name__}()"
+
+
+class FactorRowCol(Factor):
+ def _unite_all(self) -> None:
+ self._unite_rows_and_cols()
+
+
+class TargetedCellInsertionFactory(AbstractRequirementInsertionFactory):
+ """
+ Insert factors requirements or obstructions on the tiling if it can lead
+ to separating a verified factor.
+ """
+
+ def __init__(
+ self,
+ verification_strategies: Optional[Iterable[VerificationStrategy]] = None,
+ ignore_parent: bool = True,
+ ) -> None:
+ self.verification_strats: List[VerificationStrategy] = (
+ list(verification_strategies)
+ if verification_strategies is not None
+ else [
+ strat.BasicVerificationStrategy(),
+ strat.InsertionEncodingVerificationStrategy(),
+ strat.OneByOneVerificationStrategy(),
+ strat.LocallyFactorableVerificationStrategy(),
+ ]
+ )
+ super().__init__(ignore_parent)
+
+ def verified(self, tiling: Tiling) -> bool:
+ """Return True if any verification strategy verifies the tiling"""
+ return any(strategy.verified(tiling) for strategy in self.verification_strats)
+
+ def req_lists_to_insert(self, tiling: Tiling) -> Iterator[ListRequirement]:
+ factor_class = FactorRowCol(tiling)
+ potential_factors = factor_class.get_components()
+ reqs_and_obs: Set[GriddedPerm] = set(
+ chain(tiling.obstructions, *tiling.requirements)
+ )
+ for cells in potential_factors:
+ if self.verified(tiling.sub_tiling(cells)):
+ for gp in reqs_and_obs:
+ if any(cell in cells for cell in gp.pos) and any(
+ cell not in cells for cell in gp.pos
+ ):
+ yield (gp.get_gridded_perm_in_cells(cells),)
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["ver_strats"] = [
+ strategy.to_jsonable() for strategy in self.verification_strats
+ ]
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "TargetedCellInsertionFactory":
+ ver_strats = [
+ cast(VerificationStrategy, VerificationStrategy.from_dict(strategy))
+ for strategy in d["ver_strats"]
+ ]
+ return TargetedCellInsertionFactory(ver_strats, d["ignore_parent"])
+
+ def __repr__(self):
+ return (
+ self.__class__.__name__
+ + f"({self.verification_strats}, {self.ignore_parent})"
+ )
+
+ def __str__(self) -> str:
+ return "targeted cell insertions"
+
+
+class SubobstructionInsertionFactory(AbstractRequirementInsertionFactory):
+ """
+ Insert all subobstructions of the obstructions on the tiling.
+ """
+
+ def req_lists_to_insert(self, tiling: Tiling) -> Iterator[ListRequirement]:
+ for gp in SubobstructionInferral(tiling).potential_new_obs():
+ yield (gp,)
+
+ def __str__(self) -> str:
+ return "subobstruction insertion"
+
+
+class BasisPatternInsertionFactory(AbstractRequirementInsertionFactory):
+ """
+ Insert all requirements that are a subpattern of every pattern in the basis.
+ """
+
+ def __init__(
+ self,
+ basis: Optional[Iterable[Perm]] = None,
+ ignore_parent: bool = False,
+ ):
+ self.basis: Tuple[Perm, ...] = tuple(basis) if basis is not None else tuple()
+ self.perms = self.get_patterns()
+ self.maxreqlen = max(map(len, self.perms), default=0)
+ super().__init__(ignore_parent=ignore_parent)
+
+ def get_patterns(self) -> Set[Perm]:
+ res: Set[Perm] = set()
+ to_process: Iterable[Perm] = self.basis
+ while to_process:
+ to_process = set(
+ (
+ perm.remove(idx)
+ for perm in to_process
+ for idx in perm
+ if len(perm) > 1
+ )
+ )
+ res.update(to_process)
+ return set(
+ perm for perm in res if all(patt.contains(perm) for patt in self.basis)
+ )
+
+ def change_basis(
+ self,
+ basis: Iterable[Perm],
+ ) -> "BasisPatternInsertionFactory":
+ """
+ Return the version of the strategy with the given basis instead
+ of the current one.
+ """
+ return self.__class__(tuple(basis))
+
+ def req_lists_to_insert(self, tiling: Tiling) -> Iterator[ListRequirement]:
+ obs_tiling = Tiling(
+ tiling.obstructions,
+ remove_empty_rows_and_cols=False,
+ derive_empty=False,
+ simplify=False,
+ sorted_input=True,
+ )
+ for length in range(1, self.maxreqlen + 1):
+ for gp in obs_tiling.gridded_perms_of_length(length):
+ if gp.patt in self.perms:
+ yield (gp,)
+
+ def __str__(self) -> str:
+ patts = "{" + ", ".join(map(str, sorted(self.perms))) + "}"
+ return f"insertions with permutations {patts}"
diff --git a/tilings/strategies/requirement_placement.py b/tilings/strategies/requirement_placement.py
index 47777781..27a2aff7 100644
--- a/tilings/strategies/requirement_placement.py
+++ b/tilings/strategies/requirement_placement.py
@@ -10,6 +10,7 @@
from permuta.misc import DIR_EAST, DIR_NORTH, DIR_SOUTH, DIR_WEST, DIRS
from tilings import GriddedPerm, Tiling
from tilings.algorithms import RequirementPlacement
+from tilings.algorithms.fusion import Fusion
__all__ = [
"PatternPlacementFactory",
@@ -634,6 +635,50 @@ def from_dict(cls, d: dict) -> "RowAndColumnPlacementFactory":
return cls(**d)
+class FusableRowAndColumnPlacementFactory(RowAndColumnPlacementFactory):
+ def req_indices_and_directions_to_place(
+ self, tiling: Tiling
+ ) -> Iterator[Tuple[Tuple[GriddedPerm, ...], Tuple[int, ...], int]]:
+ """
+ For each row, yield the gps with size one req for each cell in a row.
+ """
+ cols: Dict[int, Set[GriddedPerm]] = defaultdict(set)
+ rows: Dict[int, Set[GriddedPerm]] = defaultdict(set)
+ for cell in tiling.active_cells:
+ gp = GriddedPerm((0,), (cell,))
+ cols[cell[0]].add(gp)
+ rows[cell[1]].add(gp)
+ if self.place_col:
+ fusable_indices = set(
+ chain.from_iterable(
+ (idx, idx + 1)
+ for idx in range(tiling.dimensions[0] - 1)
+ if Fusion(tiling, col_idx=idx).fusable()
+ )
+ )
+ fusable_cols = [cols[idx] for idx in fusable_indices]
+ col_dirs = tuple(d for d in self.dirs if d in (DIR_EAST, DIR_WEST))
+ for gps, direction in product(fusable_cols, col_dirs):
+ indices = tuple(0 for _ in gps)
+ yield tuple(gps), indices, direction
+ if self.place_row:
+ fusable_indices = set(
+ chain.from_iterable(
+ (idx, idx + 1)
+ for idx in range(tiling.dimensions[1] - 1)
+ if Fusion(tiling, row_idx=idx).fusable()
+ )
+ )
+ fusable_rows = [rows[idx] for idx in fusable_indices]
+ row_dirs = tuple(d for d in self.dirs if d in (DIR_NORTH, DIR_SOUTH))
+ for gps, direction in product(fusable_rows, row_dirs):
+ indices = tuple(0 for _ in gps)
+ yield tuple(gps), indices, direction
+
+ def __str__(self) -> str:
+ return "fusable " + super().__str__()
+
+
class AllPlacementsFactory(AbstractRequirementPlacementFactory):
PLACEMENT_STRATS: Tuple[AbstractRequirementPlacementFactory, ...] = (
diff --git a/tilings/strategies/row_and_col_separation.py b/tilings/strategies/row_and_col_separation.py
index 36a6b719..7ac3ce9c 100644
--- a/tilings/strategies/row_and_col_separation.py
+++ b/tilings/strategies/row_and_col_separation.py
@@ -44,11 +44,14 @@ def _get_cell_maps(self, tiling: Tiling) -> Tuple[CellMap, CellMap]:
forward_cell_map, backward_cell_map = res
return forward_cell_map, backward_cell_map
- def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ def decomposition_function(
+ self, comb_class: Tiling
+ ) -> Optional[Tuple[Tiling, ...]]:
"""Return the separated tiling if it separates, otherwise None."""
rcs = self.row_col_sep_algorithm(comb_class)
if rcs.separable():
return (rcs.separated_tiling(),)
+ return None
def extra_parameters(
self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
@@ -87,8 +90,7 @@ def row_col_sep_algorithm(tiling: Tiling) -> RowColSeparation:
"""Return the algorithm class using tiling."""
return RowColSeparation(tiling)
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
"""Return formal step."""
return "row and column separation"
diff --git a/tilings/strategies/symmetry.py b/tilings/strategies/symmetry.py
index 54a8fe85..7e051a87 100644
--- a/tilings/strategies/symmetry.py
+++ b/tilings/strategies/symmetry.py
@@ -1,10 +1,18 @@
import abc
from functools import partial
-from typing import Dict, Iterator, Optional, Tuple, cast
+from itertools import chain, combinations
+from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple, Type, cast
from comb_spec_searcher import StrategyFactory, SymmetryStrategy
from comb_spec_searcher.exception import StrategyDoesNotApply
+from permuta import Perm
from tilings import GriddedPerm, Tiling
+from tilings.assumptions import (
+ ComponentAssumption,
+ SkewComponentAssumption,
+ SumComponentAssumption,
+ TrackingAssumption,
+)
__all__ = ("SymmetriesFactory",)
@@ -18,6 +26,28 @@ def gp_transform(self, tiling: Tiling, gp: GriddedPerm) -> GriddedPerm:
def inverse_gp_transform(self, tiling: Tiling, gp: GriddedPerm) -> GriddedPerm:
pass
+ @staticmethod
+ def assumption_type_transform(
+ assumption: TrackingAssumption,
+ ) -> Type[TrackingAssumption]:
+ raise NotImplementedError
+
+ @staticmethod
+ def _assumption_type_swap(
+ assumption: TrackingAssumption,
+ ) -> Type[TrackingAssumption]:
+ if isinstance(assumption, ComponentAssumption):
+ if isinstance(assumption, SumComponentAssumption):
+ return SkewComponentAssumption
+ return SumComponentAssumption
+ return assumption.__class__
+
+ @staticmethod
+ def _assumption_type_identity(
+ assumption: TrackingAssumption,
+ ) -> Type[TrackingAssumption]:
+ return assumption.__class__
+
def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
return (
Tiling(
@@ -29,7 +59,9 @@ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
for req in comb_class.requirements
),
tuple(
- ass.__class__(map(partial(self.gp_transform, comb_class), ass.gps))
+ self.__class__.assumption_type_transform(ass)(
+ map(partial(self.gp_transform, comb_class), ass.gps)
+ )
for ass in comb_class.assumptions
),
remove_empty_rows_and_cols=False,
@@ -49,7 +81,9 @@ def extra_parameters(
raise StrategyDoesNotApply("Strategy does not apply")
child = children[0]
mapped_assumptions = tuple(
- ass.__class__(tuple(self.gp_transform(comb_class, gp) for gp in ass.gps))
+ self.__class__.assumption_type_transform(ass)(
+ tuple(self.gp_transform(comb_class, gp) for gp in ass.gps)
+ )
for ass in comb_class.assumptions
)
return (
@@ -103,8 +137,9 @@ def reverse_cell(cell):
def inverse_gp_transform(self, tiling: Tiling, gp: GriddedPerm) -> GriddedPerm:
return self.gp_transform(tiling, gp)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_swap
+
+ def formal_step(self) -> str:
return "reverse of the tiling"
def __str__(self) -> str:
@@ -125,8 +160,9 @@ def complement_cell(cell):
def inverse_gp_transform(self, tiling: Tiling, gp: GriddedPerm) -> GriddedPerm:
return self.gp_transform(tiling, gp)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_swap
+
+ def formal_step(self) -> str:
return "complement of the tiling"
def __str__(self) -> str:
@@ -147,8 +183,9 @@ def inverse_cell(cell):
def inverse_gp_transform(self, tiling: Tiling, gp: GriddedPerm) -> GriddedPerm:
return self.gp_transform(tiling, gp)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_identity
+
+ def formal_step(self) -> str:
return "inverse of the tiling"
def __str__(self) -> str:
@@ -178,8 +215,9 @@ def antidiagonal_cell(cell):
return gp.antidiagonal(antidiagonal_cell)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_identity
+
+ def formal_step(self) -> str:
return "antidiagonal of the tiling"
def __str__(self) -> str:
@@ -203,8 +241,9 @@ def rotate270_cell(cell):
return gp.rotate270(rotate270_cell)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_swap
+
+ def formal_step(self) -> str:
return "rotate the tiling 90 degrees clockwise"
def __str__(self) -> str:
@@ -228,8 +267,9 @@ def rotate180_cell(cell):
def inverse_gp_transform(self, tiling: Tiling, gp: GriddedPerm) -> GriddedPerm:
return self.gp_transform(tiling, gp)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_identity
+
+ def formal_step(self) -> str:
return "rotate the tiling 180 degrees clockwise"
def __str__(self) -> str:
@@ -253,8 +293,9 @@ def rotate90_cell(cell):
return gp.rotate90(rotate90_cell)
- @staticmethod
- def formal_step() -> str:
+ assumption_type_transform = TilingSymmetryStrategy._assumption_type_swap
+
+ def formal_step(self) -> str:
return "rotate the tiling 270 degrees clockwise"
def __str__(self) -> str:
@@ -263,47 +304,120 @@ def __str__(self) -> str:
class SymmetriesFactory(StrategyFactory[Tiling]):
"""
- Yield all symmetry strategies for a tiling.
+ Yield symmetry strategies such that all of the underlying patterns of
+ obstructions of the symmetric tiling are subpatterns of the given basis.
"""
- def __call__(self, comb_class: Tiling) -> Iterator[TilingSymmetryStrategy]:
- def strategy(rotations: int, inverse: bool) -> TilingSymmetryStrategy:
- # pylint: disable=too-many-return-statements
- if rotations == 0:
- if inverse:
- return TilingInverse()
- if rotations == 1:
- if inverse:
- return TilingReverse()
- return TilingRotate90()
- if rotations == 2:
- if inverse:
- return TilingAntidiagonal()
- return TilingRotate180()
- if rotations == 3:
- if inverse:
- return TilingComplement()
- return TilingRotate270()
-
- symmetries = set([comb_class])
- for rotations in range(4):
- if comb_class not in symmetries:
- yield strategy(rotations, False)
- symmetries.add(comb_class)
- comb_class_inverse = comb_class.inverse()
- if comb_class_inverse not in symmetries:
- yield strategy(rotations, True)
- symmetries.add(comb_class_inverse)
- comb_class = comb_class.rotate90()
- if comb_class in symmetries:
- break
+ def __init__(
+ self,
+ basis: Optional[Iterable[Perm]] = None,
+ ):
+ self._basis = tuple(basis) if basis is not None else None
+ if self._basis is not None:
+ assert all(
+ isinstance(p, Perm) for p in self._basis
+ ), "Element of the basis must be Perm"
+ self.subpatterns: Set[Perm] = set(
+ chain.from_iterable(self._subpatterns(p) for p in self._basis)
+ )
+ self.acceptablesubpatterns: List[Set[Perm]] = [
+ set(p for p in self.subpatterns if p.rotate() in self.subpatterns),
+ set(p for p in self.subpatterns if p.rotate(2) in self.subpatterns),
+ set(p for p in self.subpatterns if p.rotate(3) in self.subpatterns),
+ set(p for p in self.subpatterns if p.inverse() in self.subpatterns),
+ set(p for p in self.subpatterns if p.reverse() in self.subpatterns),
+ set(
+ p
+ for p in self.subpatterns
+ if p.flip_antidiagonal() in self.subpatterns
+ ),
+ set(p for p in self.subpatterns if p.complement() in self.subpatterns),
+ ]
+ super().__init__()
+
+ def __call__(self, tiling: Tiling) -> Iterator[TilingSymmetryStrategy]:
+ underlying_patts = set(gp.patt for gp in tiling.obstructions)
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[0] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingRotate90()
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[1] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingRotate180()
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[2] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingRotate270()
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[3] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingInverse()
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[4] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingReverse()
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[5] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingAntidiagonal()
+ if (
+ self._basis is None
+ or all(p in self.acceptablesubpatterns[6] for p in underlying_patts)
+ or self._basis is None
+ ):
+ yield TilingComplement()
- def __str__(self) -> str:
- return "all symmetries"
+ @staticmethod
+ def _subpatterns(perm: Perm) -> Iterator[Perm]:
+ for n in range(len(perm) + 1):
+ for indices in combinations(range(len(perm)), n):
+ yield Perm.to_standard([perm[i] for i in indices])
- def __repr__(self) -> str:
- return self.__class__.__name__ + "()"
+ def change_basis(
+ self,
+ basis: Iterable[Perm],
+ ) -> "SymmetriesFactory":
+ """
+ Return the version of the strategy with the given basis instead
+ of the current one.
+ """
+ return self.__class__(tuple(basis))
+
+ @property
+ def basis(self) -> Optional[Tuple[Perm, ...]]:
+ return self._basis
+
+ def to_jsonable(self) -> dict:
+ d: dict = super().to_jsonable()
+ d["basis"] = self._basis
+ return d
@classmethod
def from_dict(cls, d: dict) -> "SymmetriesFactory":
- return cls()
+ if "basis" in d and d["basis"] is not None:
+ basis: Optional[List[Perm]] = [Perm(p) for p in d.pop("basis")]
+ else:
+ basis = d.pop("basis", None)
+ return cls(basis=basis)
+
+ def __str__(self) -> str:
+ if self._basis is not None:
+ basis = ", ".join(str(p) for p in self._basis)
+ return f"symmetries in Av({basis})"
+ return "all symmetries"
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(basis={self._basis})"
diff --git a/tilings/strategies/unfusion.py b/tilings/strategies/unfusion.py
new file mode 100644
index 00000000..3b9be3a9
--- /dev/null
+++ b/tilings/strategies/unfusion.py
@@ -0,0 +1,334 @@
+from collections import Counter
+from itertools import chain
+from typing import Callable, Dict, Iterator, List, Optional, Tuple
+
+import sympy
+
+from comb_spec_searcher import Strategy
+from comb_spec_searcher.strategies import Constructor
+from comb_spec_searcher.strategies.constructor import Complement, DisjointUnion
+from comb_spec_searcher.strategies.strategy import StrategyFactory
+from comb_spec_searcher.typing import (
+ CombinatorialClassType,
+ CombinatorialObjectType,
+ Parameters,
+ SubObjects,
+ SubRecs,
+ SubSamplers,
+ SubTerms,
+ Terms,
+)
+from tilings import GriddedPerm, Tiling
+from tilings.algorithms import Fusion
+
+
+class DivideByN(DisjointUnion[CombinatorialClassType, CombinatorialObjectType]):
+ """
+ A constructor that works as disjoint union
+ but divides the values by n + shift.
+ """
+
+ def __init__(
+ self,
+ parent: CombinatorialClassType,
+ children: Tuple[CombinatorialClassType, ...],
+ shift: int,
+ extra_parameters: Optional[Tuple[Dict[str, str], ...]] = None,
+ ):
+ self.shift = shift
+ self.initial_conditions = {
+ n: parent.get_terms(n) for n in range(1 - self.shift)
+ }
+ super().__init__(parent, children, extra_parameters)
+
+ def get_equation(
+ self, lhs_func: sympy.Function, rhs_funcs: Tuple[sympy.Function, ...]
+ ) -> sympy.Eq:
+ # TODO: d/dx [ x**shift * lhsfun ] / x**(shift - 1) = A + B + ...
+ raise NotImplementedError
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ if n + self.shift <= 0:
+ return self.initial_conditions[n]
+ terms = super().get_terms(parent_terms, subterms, n)
+ return Counter({key: value // (n + self.shift) for key, value in terms.items()})
+
+ def get_sub_objects(
+ self, subobjs: SubObjects, n: int
+ ) -> Iterator[
+ Tuple[Parameters, Tuple[List[Optional[CombinatorialObjectType]], ...]]
+ ]:
+ raise NotImplementedError
+
+ def random_sample_sub_objects(
+ self,
+ parent_count: int,
+ subsamplers: SubSamplers,
+ subrecs: SubRecs,
+ n: int,
+ **parameters: int,
+ ) -> Tuple[Optional[CombinatorialObjectType], ...]:
+ raise NotImplementedError
+
+ @staticmethod
+ def get_eq_symbol() -> str:
+ return "?"
+
+ def __str__(self):
+ return "divide by n"
+
+ def equiv(
+ self, other: Constructor, data: Optional[object] = None
+ ) -> Tuple[bool, Optional[object]]:
+ raise NotImplementedError
+
+
+class ReverseDivideByN(Complement[CombinatorialClassType, CombinatorialObjectType]):
+ """
+ The complement version of DivideByN.
+ It works as Complement, but multiplies by n + shift the original left hand side.
+ """
+
+ def __init__(
+ self,
+ parent: CombinatorialClassType,
+ children: Tuple[CombinatorialClassType, ...],
+ idx: int,
+ shift: int,
+ extra_parameters: Optional[Tuple[Dict[str, str], ...]] = None,
+ ):
+ self.shift = shift
+ self.initial_conditions = {
+ n: children[idx].get_terms(n) for n in range(1 - self.shift)
+ }
+ super().__init__(parent, children, idx, extra_parameters)
+
+ def get_equation(
+ self, lhs_func: sympy.Function, rhs_funcs: Tuple[sympy.Function, ...]
+ ) -> sympy.Eq:
+ # TODO: rhs_funcs[0] should be a derivative etc, see DivideByN.get_equation.
+ raise NotImplementedError
+
+ def get_terms(
+ self, parent_terms: Callable[[int], Terms], subterms: SubTerms, n: int
+ ) -> Terms:
+ if n + self.shift <= 0:
+ return self.initial_conditions[n]
+ parent_terms_mapped: Terms = Counter()
+ for param, value in subterms[0](n).items():
+ if value:
+ # This is the only change from complement
+ N = n + self.shift
+ assert N > 0
+ parent_terms_mapped[self._parent_param_map(param)] += value * N
+ children_terms = subterms[1:]
+ for child_terms, param_map in zip(children_terms, self._children_param_maps):
+ # we subtract from total
+ for param, value in child_terms(n).items():
+ mapped_param = self._parent_param_map(param_map(param))
+ parent_terms_mapped[mapped_param] -= value
+ assert parent_terms_mapped[mapped_param] >= 0
+ if parent_terms_mapped[mapped_param] == 0:
+ parent_terms_mapped.pop(mapped_param)
+
+ return parent_terms_mapped
+
+ def __str__(self):
+ return "reverse divide by n"
+
+
+class UnfusionColumnStrategy(Strategy[Tiling, GriddedPerm]):
+ def __init__(
+ self,
+ ignore_parent: bool = False,
+ inferrable: bool = True,
+ possibly_empty: bool = True,
+ workable: bool = True,
+ cols: bool = True,
+ ):
+ self.cols = cols
+ super().__init__(ignore_parent, inferrable, possibly_empty, workable)
+
+ def can_be_equivalent(self) -> bool:
+ return False
+
+ def is_two_way(self, comb_class: Tiling) -> bool:
+ return True
+
+ def is_reversible(self, comb_class: Tiling) -> bool:
+ return True
+
+ def shifts(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]],
+ ) -> Tuple[int, ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return tuple(0 for _ in range(self.width(comb_class)))
+
+ def width(self, comb_class: Tiling) -> int:
+ if self.cols:
+ return comb_class.dimensions[0]
+ return comb_class.dimensions[1]
+
+ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling, ...]:
+ res = []
+ for idx in range(self.width(comb_class)):
+ if self.cols:
+ algo = Fusion(comb_class, col_idx=idx)
+ else:
+ algo = Fusion(comb_class, row_idx=idx)
+ obs = chain(
+ *[algo.unfuse_gridded_perm(ob) for ob in comb_class.obstructions]
+ )
+ reqs = [
+ [gp for req_gp in req_list for gp in algo.unfuse_gridded_perm(req_gp)]
+ for req_list in comb_class.requirements
+ ]
+ ass = [
+ ass.__class__(
+ [
+ gp
+ for ass_gp in ass.gps
+ for gp in algo.unfuse_gridded_perm(ass_gp)
+ ]
+ )
+ for ass in comb_class.assumptions
+ ]
+ res.append(Tiling(obs, reqs, ass))
+ return tuple(res)
+
+ def formal_step(self) -> str:
+ if self.cols:
+ return "unfuse columns strategy"
+ return "unfuse rows strategy"
+
+ def constructor(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> DivideByN:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return DivideByN(
+ comb_class,
+ children,
+ len(children),
+ self.extra_parameters(comb_class, children),
+ )
+
+ def reverse_constructor(
+ self,
+ idx: int,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> ReverseDivideByN:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ return ReverseDivideByN(
+ comb_class,
+ children,
+ idx,
+ len(children),
+ self.extra_parameters(comb_class, children),
+ )
+
+ def backward_map(
+ self,
+ comb_class: Tiling,
+ objs: Tuple[Optional[GriddedPerm], ...],
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Iterator[GriddedPerm]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def forward_map(
+ self,
+ comb_class: Tiling,
+ obj: GriddedPerm,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Optional[GriddedPerm], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ raise NotImplementedError
+
+ def extra_parameters(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> Tuple[Dict[str, str], ...]:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ res = []
+ for idx in range(self.width(comb_class)):
+ if self.cols:
+ algo = Fusion(comb_class, col_idx=idx)
+ else:
+ algo = Fusion(comb_class, row_idx=idx)
+ params: Dict[str, str] = {}
+ for ass in comb_class.assumptions:
+ mapped_ass = ass.__class__(
+ [
+ children[idx].forward_map.map_gp(gp)
+ for ass_gp in ass.gps
+ for gp in algo.unfuse_gridded_perm(ass_gp)
+ ]
+ )
+ params[comb_class.get_assumption_parameter(ass)] = children[
+ idx
+ ].get_assumption_parameter(mapped_ass)
+ res.append(params)
+ return tuple(res)
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "UnfusionColumnStrategy":
+ return cls(**d)
+
+
+class UnfusionRowStrategy(UnfusionColumnStrategy):
+ def __init__(
+ self,
+ ignore_parent: bool = False,
+ inferrable: bool = True,
+ possibly_empty: bool = True,
+ workable: bool = True,
+ cols: bool = False,
+ ):
+ super().__init__(ignore_parent, inferrable, possibly_empty, workable, cols)
+
+
+class UnfusionFactory(StrategyFactory[Tiling]):
+ def __init__(self, max_width: int = 4, max_height: int = 4) -> None:
+ self.max_width = max_width
+ self.max_height = max_height
+ super().__init__()
+
+ def __call__(self, comb_class: Tiling) -> Iterator[UnfusionColumnStrategy]:
+ width, height = comb_class.dimensions
+ if width <= self.max_width:
+ yield UnfusionColumnStrategy()
+ if height <= self.max_height:
+ yield UnfusionRowStrategy()
+
+ def __str__(self) -> str:
+ return "unfusion strategy"
+
+ def __repr__(self) -> str:
+ return (
+ self.__class__.__name__
+ + f"(max_width={self.max_width}, max_height={self.max_height})"
+ )
+
+ def to_jsonable(self) -> dict:
+ d = super().to_jsonable()
+ d["max_width"] = self.max_width
+ d["max_height"] = self.max_height
+ return d
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "UnfusionFactory":
+ return cls(max_width=d["max_width"], max_height=d["max_height"])
diff --git a/tilings/strategies/verification.py b/tilings/strategies/verification.py
index 5d41ada9..faa4e667 100644
--- a/tilings/strategies/verification.py
+++ b/tilings/strategies/verification.py
@@ -2,22 +2,26 @@
from functools import reduce
from itertools import chain
from operator import mul
-from typing import Dict, Iterator, Optional, Tuple, cast
+from typing import Any, Callable, Dict, Iterable, Iterator, Optional, Tuple, cast
-from sympy import Expr, Function, var
+import requests
+from sympy import Eq, Expr, Function, Symbol, collect, degree, solve, sympify, var
from comb_spec_searcher import (
AtomStrategy,
CombinatorialClass,
+ CombinatorialSpecification,
StrategyPack,
VerificationStrategy,
)
from comb_spec_searcher.exception import InvalidOperationError, StrategyDoesNotApply
+from comb_spec_searcher.strategies import VerificationRule
from comb_spec_searcher.typing import Objects, Terms
from permuta import Av, Perm
from permuta.permutils import (
is_insertion_encodable_maximum,
is_insertion_encodable_rightmost,
+ lex_min,
)
from tilings import GriddedPerm, Tiling
from tilings.algorithms import locally_factorable_shift
@@ -26,12 +30,14 @@
LocalEnumeration,
MonotoneTreeEnumeration,
)
-from tilings.assumptions import ComponentAssumption
+from tilings.assumptions import ComponentAssumption, TrackingAssumption
from tilings.strategies import (
+ DetectComponentsStrategy,
FactorFactory,
FactorInsertionFactory,
RemoveRequirementFactory,
RequirementCorroborationFactory,
+ SymmetriesFactory,
)
from .abstract import BasisAwareVerificationStrategy
@@ -57,8 +63,7 @@ class BasicVerificationStrategy(AtomStrategy):
TODO: can this be moved to the CSS atom strategy?
"""
- @staticmethod
- def get_terms(comb_class: CombinatorialClass, n: int) -> Terms:
+ def get_terms(self, comb_class: CombinatorialClass, n: int) -> Terms:
if not isinstance(comb_class, Tiling):
raise NotImplementedError
gp = next(comb_class.minimal_gridded_perms())
@@ -69,8 +74,7 @@ def get_terms(comb_class: CombinatorialClass, n: int) -> Terms:
return Counter([parameters])
return Counter()
- @staticmethod
- def get_objects(comb_class: CombinatorialClass, n: int) -> Objects:
+ def get_objects(self, comb_class: CombinatorialClass, n: int) -> Objects:
if not isinstance(comb_class, Tiling):
raise NotImplementedError
res: Objects = defaultdict(list)
@@ -91,22 +95,26 @@ def generate_objects_of_size(
"""
yield from comb_class.objects_of_size(n, **parameters)
- @staticmethod
def random_sample_object_of_size(
- comb_class: CombinatorialClass, n: int, **parameters: int
+ self, comb_class: CombinatorialClass, n: int, **parameters: int
) -> GriddedPerm:
"""
Verification strategies must contain a method to sample the objects.
"""
key = tuple(y for _, y in sorted(parameters.items()))
- if BasicVerificationStrategy.get_terms(comb_class, n).get(key):
+ if BasicVerificationStrategy().get_terms(comb_class, n).get(key):
return cast(GriddedPerm, next(comb_class.objects_of_size(n, **parameters)))
+ raise (
+ NotImplementedError(
+ "Verification strategy did not contain a method to sample the objects"
+ )
+ )
def get_genf(
self,
comb_class: CombinatorialClass,
funcs: Optional[Dict[CombinatorialClass, Function]] = None,
- ) -> Expr:
+ ) -> Any:
if not self.verified(comb_class):
raise StrategyDoesNotApply("Can't find generating functon for non-atom.")
if not isinstance(comb_class, Tiling):
@@ -124,13 +132,165 @@ def __repr__(self) -> str:
return f"{self.__class__.__name__}()"
+class OneByOneVerificationRule(VerificationRule[Tiling, GriddedPerm]):
+ def get_equation(
+ self,
+ get_function: Callable[[Tiling], Function],
+ funcs: Optional[Dict[Tiling, Function]] = None,
+ ) -> Eq:
+ # Find the minimal polynomial for the underlying class
+ basis = [ob.patt for ob in self.comb_class.obstructions]
+ basis_str = "_".join(map(str, lex_min(basis)))
+ uri = f"https://permpal.com/perms/raw_data_json/basis/{basis_str}"
+ request = requests.get(uri, timeout=10)
+ if request.status_code == 404:
+ return super().get_equation(get_function, funcs)
+ data = request.json()
+ min_poly = data["min_poly_maple"]
+ if min_poly is None:
+ return Eq(
+ get_function(self.comb_class),
+ self.tiling_to_symbol_eq(self.comb_class),
+ )
+ min_poly = min_poly.replace("^", "**").replace("F(x)", "F")
+ lhs, _ = min_poly.split("=")
+ # We now need to worry about the requirements. The min poly we got is
+ # for the class with requirements.
+ eq = Eq(self.without_req_genf(self.comb_class), get_function(self.comb_class))
+ subs = solve([eq], var("F"), dict=True)[0]
+ if self.comb_class.assumptions:
+ subs["x"] = var("x") * var("k_0")
+ res, _ = sympify(lhs).subs(subs, simultaneous=True).as_numer_denom()
+ # Pick the unique factor that contains F
+ for factor in res.as_ordered_factors():
+ if factor.atoms(Function):
+ res = factor
+ # currently we have 0 = rhs,
+ lhs = get_function(self.comb_class)
+ if degree(res, lhs) == 1:
+ # solve for rational gf
+ rhs = solve([Eq(res, 0)], lhs, dict=True)[0][lhs]
+ else:
+ # or add F to both sides
+ rhs = collect(res + lhs, lhs)
+ return Eq(lhs, rhs)
+
+ def tiling_to_symbol_eq(self, tiling: Tiling) -> Any:
+ """
+ Find the equation for the tiling in terms of F_C's, where C are
+ permutation classes.
+ """
+ if tiling.requirements:
+ reqs = tiling.requirements[0]
+ avoided = tiling.__class__(
+ tiling.obstructions + reqs,
+ tiling.requirements[1:],
+ tiling.assumptions,
+ )
+ without = tiling.__class__(
+ tiling.obstructions,
+ tiling.requirements[1:],
+ tiling.assumptions,
+ )
+ return self.tiling_to_symbol_eq(without) - self.tiling_to_symbol_eq(avoided)
+ params = self.comb_class.extra_parameters
+ x_var = "x"
+ if params:
+ assert len(params) == 1
+ x_var += "*" + params[0]
+ basis = [ob.patt for ob in tiling.obstructions]
+ one_based_basis = ",".join(("".join(str(i + 1) for i in b) for b in basis))
+ return Symbol(f"F_Av({one_based_basis})({x_var})")
+
+ @property
+ def no_req_tiling(self) -> Tiling:
+ return self.comb_class.__class__(
+ self.comb_class.obstructions, tuple(), self.comb_class.assumptions
+ )
+
+ def without_req_genf(self, tiling: Tiling):
+ """
+ Find the equation for the tiling in terms of F, the generating
+ function where the reqs are reomoved from tiling.
+ """
+ if tiling == self.no_req_tiling:
+ return var("F")
+ if tiling.requirements:
+ reqs = tiling.requirements[0]
+ avoided = tiling.__class__(
+ tiling.obstructions + reqs,
+ tiling.requirements[1:],
+ tiling.assumptions,
+ )
+ without = tiling.__class__(
+ tiling.obstructions,
+ tiling.requirements[1:],
+ tiling.assumptions,
+ )
+ avgf = self.without_req_genf(avoided)
+ wogf = self.without_req_genf(without)
+ return wogf - avgf
+ return LocalEnumeration(tiling).get_genf()
+
+
class OneByOneVerificationStrategy(BasisAwareVerificationStrategy):
+ def __init__(
+ self,
+ basis: Optional[Iterable[Perm]] = None,
+ symmetry: bool = False,
+ ignore_parent: bool = False,
+ ):
+ super().__init__(basis, symmetry, ignore_parent)
+ self._spec: Dict[Tiling, CombinatorialSpecification] = {}
+
@staticmethod
- def pack(comb_class: Tiling) -> StrategyPack:
+ def _spec_from_permpal(tiling: Tiling) -> CombinatorialSpecification:
+ basis = [ob.patt for ob in tiling.obstructions]
+ basis_str = "_".join(map(str, lex_min(basis)))
+ uri = f"https://permpal.com/perms/raw_data_json/basis/{basis_str}"
+ request = requests.get(uri, timeout=10)
+ if request.status_code == 404:
+ raise InvalidOperationError("Can't find spec for one by one verified rule.")
+ data = request.json()
+ spec_json = data["specs_and_eqs"][0]["spec_json"]
+ spec = cast(
+ CombinatorialSpecification, CombinatorialSpecification.from_dict(spec_json)
+ )
+ actual_class = Tiling(tiling.obstructions)
+ if spec.root != actual_class:
+ for strategy in SymmetriesFactory()(actual_class):
+ rule = strategy(actual_class)
+ if rule.children[0] == spec.root:
+ break
+ else:
+ raise InvalidOperationError("Error fixing sym in 1x1")
+ rules = [rule] + list(spec.rules_dict.values())
+ spec = CombinatorialSpecification(rule.comb_class, rules)
+ assert spec.root == Tiling(tiling.obstructions)
+ return spec
+
+ def get_specification(
+ self, comb_class: Tiling
+ ) -> CombinatorialSpecification[Tiling, GriddedPerm]:
+ if comb_class not in self._spec:
+ try:
+ self._spec[comb_class] = super().get_specification(comb_class)
+ except InvalidOperationError as e:
+ if len(comb_class.requirements) > 1 or comb_class.dimensions != (1, 1):
+ raise e
+ self._spec[comb_class] = self._spec_from_permpal(comb_class)
+ return self._spec[comb_class]
+
+ def get_complement_spec(self, tiling: Tiling) -> CombinatorialSpecification:
+ assert len(tiling.requirements) == 1
+ complement = tiling.remove_requirement(tiling.requirements[0]).add_obstructions(
+ tiling.requirements[0]
+ )
+ return self.get_specification(complement)
+
+ def pack(self, comb_class: Tiling) -> StrategyPack:
if any(isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions):
- raise InvalidOperationError(
- "Can't find generating function with component assumption."
- )
+ return ComponentVerificationStrategy().pack(comb_class)
# pylint: disable=import-outside-toplevel
from tilings.tilescope import TileScopePack
@@ -159,15 +319,10 @@ def pack(comb_class: Tiling) -> StrategyPack:
and len(comb_class.requirements[0]) == 1
and len(comb_class.requirements[0][0]) <= 2
):
- if basis in ([Perm((0, 1, 2))], [Perm((2, 1, 0))]):
- # Av(123) or Av(321) - use fusion!
- return (
- TileScopePack.row_and_col_placements(row_only=True)
- .make_fusion(tracked=True)
- .add_basis(basis)
- )
- if (Perm((0, 1, 2)) in basis or Perm((2, 1, 0)) in basis) and all(
- len(p) <= 4 for p in basis
+ if (
+ (Perm((0, 1, 2)) in basis or Perm((2, 1, 0)) in basis)
+ and all(len(p) <= 4 for p in basis)
+ and len(basis) > 1
):
# is a subclass of Av(123) avoiding patterns of length <= 4
# experimentally showed that such clsses always terminates
@@ -177,6 +332,17 @@ def pack(comb_class: Tiling) -> StrategyPack:
f"subclass Av({basis})"
)
+ def __call__(
+ self,
+ comb_class: Tiling,
+ children: Optional[Tuple[Tiling, ...]] = None,
+ ) -> OneByOneVerificationRule:
+ if children is None:
+ children = self.decomposition_function(comb_class)
+ if children is None:
+ raise StrategyDoesNotApply("The combinatorial class is not verified")
+ return OneByOneVerificationRule(self, comb_class, children)
+
def verified(self, comb_class: Tiling) -> bool:
if not comb_class.dimensions == (1, 1):
return False
@@ -187,13 +353,11 @@ def verified(self, comb_class: Tiling) -> bool:
is_strict_subclass = any(
tiling_class.is_subclass(cls) and cls != tiling_class for cls in sym_classes
)
- return is_strict_subclass or any(
- isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions
- )
+ return is_strict_subclass
def get_genf(
self, comb_class: Tiling, funcs: Optional[Dict[Tiling, Function]] = None
- ) -> Expr:
+ ) -> Any:
if not self.verified(comb_class):
raise StrategyDoesNotApply("tiling not 1x1 verified")
if len(comb_class.obstructions) == 1 and comb_class.obstructions[0] in (
@@ -206,35 +370,137 @@ def get_genf(
except InvalidOperationError:
return LocalEnumeration(comb_class).get_genf(funcs=funcs)
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling is a subclass of the original tiling"
- @staticmethod
- def get_terms(comb_class: Tiling, n: int) -> Terms:
+ def get_terms(self, comb_class: Tiling, n: int) -> Terms:
+ terms = super().get_terms(comb_class.remove_assumptions(), n)
+ if (
+ comb_class.requirements
+ and self.get_specification(comb_class).root != comb_class
+ ):
+ if len(comb_class.requirements) == 1:
+ comp_spec = self.get_complement_spec(comb_class.remove_assumptions())
+ else:
+ raise NotImplementedError(
+ "Not implemented counting for one by one with two or more reqs"
+ )
+ comp_terms = comp_spec.get_terms(n)
+ terms = Counter({tuple(): terms[tuple()] - comp_terms[tuple()]})
+ if comb_class.assumptions:
+ assert comb_class.assumptions == (TrackingAssumption.from_cells([(0, 0)]),)
+ terms = Counter({(n,): terms[tuple()]})
+ return terms
+
+ def get_objects(self, comb_class: Tiling, n: int) -> Objects:
+ objects = super().get_objects(comb_class, n)
+ if comb_class.requirements:
+ if len(comb_class.requirements) == 1:
+ comp_spec = self.get_complement_spec(comb_class.remove_assumptions())
+ else:
+ raise NotImplementedError(
+ "Not implemented objects for one by one with two or more reqs"
+ )
+ comp_objects = comp_spec.get_objects(n)
+ objects = defaultdict(
+ list,
+ {
+ a: list(set(b).difference(comp_objects[a]))
+ for a, b in objects.items()
+ },
+ )
+
+ if comb_class.assumptions:
+ assert comb_class.assumptions == (TrackingAssumption.from_cells([(0, 0)]),)
+ objects = defaultdict(list, {(n,): objects[tuple()]})
+ return objects
+
+ def random_sample_object_of_size(
+ self, comb_class: Tiling, n: int, **parameters: int
+ ) -> GriddedPerm:
+ if comb_class.assumptions:
+ assert (
+ len(comb_class.assumptions) == 1
+ and parameters[
+ comb_class.get_assumption_parameter(comb_class.assumptions[0])
+ ]
+ == n
+ )
+ while True:
+ # Rejection sampling
+ gp = super().random_sample_object_of_size(
+ Tiling(comb_class.obstructions), n
+ )
+ if gp in comb_class:
+ return gp
+
+ def __str__(self) -> str:
+ if not self.basis:
+ return "one by one verification"
+ return f"One by one subclass of {Av(self.basis)}"
+
+
+class ComponentVerificationStrategy(TileScopeVerificationStrategy):
+ """Enumeration strategy for verifying 1x1s with component assumptions."""
+
+ def pack(self, comb_class: Tiling) -> StrategyPack:
+ raise InvalidOperationError("No pack for removing component assumption")
+
+ def verified(self, comb_class: Tiling) -> bool:
+ return comb_class.dimensions == (1, 1) and any(
+ isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions
+ )
+
+ def decomposition_function(
+ self, comb_class: Tiling
+ ) -> Optional[Tuple[Tiling, ...]]:
+ """
+ The rule as the root as children if one of the cell of the tiling is the root.
+ """
+ if self.verified(comb_class):
+ return (comb_class.remove_assumptions(),)
+ return None
+
+ def shifts(
+ self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None
+ ) -> Tuple[int, ...]:
+ return (0,)
+
+ def formal_step(self) -> str:
+ return "component verified"
+
+ def get_genf(
+ self, comb_class: Tiling, funcs: Optional[Dict[Tiling, Function]] = None
+ ) -> Expr:
+ raise NotImplementedError(
+ "Not implemented method to count objects for component verified"
+ )
+
+ def get_terms(self, comb_class: Tiling, n: int) -> Terms:
raise NotImplementedError(
- "Not implemented method to count objects for one by one verified tilings"
+ "Not implemented method to count objects for component verified"
)
def generate_objects_of_size(
self, comb_class: Tiling, n: int, **parameters: int
) -> Iterator[GriddedPerm]:
raise NotImplementedError(
- "Not implemented method to generate objects for one by one "
- "verified tilings"
+ "Not implemented method to generate objects for component verified tilings"
)
def random_sample_object_of_size(
self, comb_class: Tiling, n: int, **parameters: int
) -> GriddedPerm:
raise NotImplementedError(
- "Not implemented random sample for one by one verified tilings"
+ "Not implemented random sample for component verified tilings"
)
def __str__(self) -> str:
- if not self.basis:
- return "one by one verification"
- return f"One by one subclass of {Av(self.basis)}"
+ return "component verification"
+
+ @classmethod
+ def from_dict(cls, d: dict) -> "ComponentVerificationStrategy":
+ return cls(**d)
class DatabaseVerificationStrategy(TileScopeVerificationStrategy):
@@ -245,30 +511,26 @@ class DatabaseVerificationStrategy(TileScopeVerificationStrategy):
can always find the generating function by looking up the database.
"""
- @staticmethod
- def pack(comb_class: Tiling) -> StrategyPack:
+ def pack(self, comb_class: Tiling) -> StrategyPack:
# TODO: check database for tiling
raise InvalidOperationError(
"Cannot get a specification for a tiling in the database"
)
- @staticmethod
- def verified(comb_class: Tiling):
+ def verified(self, comb_class: Tiling):
return DatabaseEnumeration(comb_class).verified()
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling is in the database"
def get_genf(
self, comb_class: Tiling, funcs: Optional[Dict[Tiling, Function]] = None
- ) -> Expr:
+ ) -> Any:
if not self.verified(comb_class):
raise StrategyDoesNotApply("tiling is not in the database")
return DatabaseEnumeration(comb_class).get_genf()
- @staticmethod
- def get_terms(comb_class: Tiling, n: int) -> Terms:
+ def get_terms(self, comb_class: Tiling, n: int) -> Terms:
raise NotImplementedError(
"Not implemented method to count objects for database verified tilings"
)
@@ -309,13 +571,13 @@ class LocallyFactorableVerificationStrategy(BasisAwareVerificationStrategy):
"""
def pack(self, comb_class: Tiling) -> StrategyPack:
- if any(isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions):
- raise InvalidOperationError(
- "Can't find generating function with component assumption."
- )
return StrategyPack(
name="LocallyFactorable",
- initial_strats=[FactorFactory(), RequirementCorroborationFactory()],
+ initial_strats=[
+ FactorFactory(),
+ RequirementCorroborationFactory(),
+ DetectComponentsStrategy(),
+ ],
inferral_strats=[],
expansion_strats=[[FactorInsertionFactory()], [RemoveRequirementFactory()]],
ver_strats=[
@@ -323,6 +585,7 @@ def pack(self, comb_class: Tiling) -> StrategyPack:
OneByOneVerificationStrategy(
basis=self._basis, symmetry=self._symmetry
),
+ ComponentVerificationStrategy(),
InsertionEncodingVerificationStrategy(),
MonotoneTreeVerificationStrategy(no_factors=True),
LocalVerificationStrategy(no_factors=True),
@@ -331,18 +594,19 @@ def pack(self, comb_class: Tiling) -> StrategyPack:
@staticmethod
def _pack_for_shift(comb_class: Tiling) -> StrategyPack:
- if any(isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions):
- raise InvalidOperationError(
- "Can't find generating function with component assumption."
- )
return StrategyPack(
name="LocallyFactorable",
- initial_strats=[FactorFactory(), RequirementCorroborationFactory()],
+ initial_strats=[
+ FactorFactory(),
+ RequirementCorroborationFactory(),
+ DetectComponentsStrategy(),
+ ],
inferral_strats=[],
expansion_strats=[[FactorInsertionFactory()]],
ver_strats=[
BasicVerificationStrategy(),
OneByOneVerificationStrategy(),
+ ComponentVerificationStrategy(),
InsertionEncodingVerificationStrategy(),
MonotoneTreeVerificationStrategy(no_factors=True),
LocalVerificationStrategy(no_factors=True),
@@ -369,6 +633,14 @@ def verified(self, comb_class: Tiling):
not comb_class.dimensions == (1, 1)
and self._locally_factorable_obstructions(comb_class)
and self._locally_factorable_requirements(comb_class)
+ and all(
+ not isinstance(ass, ComponentAssumption)
+ or (
+ len(ass.gps) == 1
+ and comb_class.only_cell_in_row_and_col(list(ass.cells)[0])
+ )
+ for ass in comb_class.assumptions
+ )
)
def decomposition_function(
@@ -405,8 +677,7 @@ def shifts(
assert shift is not None
return (shift,)
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling is locally factorable"
def __str__(self) -> str:
@@ -427,12 +698,18 @@ class ElementaryVerificationStrategy(LocallyFactorableVerificationStrategy):
verified tiling.
"""
- @staticmethod
- def verified(comb_class: Tiling):
- return comb_class.fully_isolated() and not comb_class.dimensions == (1, 1)
+ def verified(self, comb_class: Tiling):
+ return (
+ comb_class.fully_isolated()
+ and not comb_class.dimensions == (1, 1)
+ and all(
+ len(ass.gps) == 1
+ for ass in comb_class.assumptions
+ if isinstance(ass, ComponentAssumption)
+ )
+ )
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling is elementary verified"
@classmethod
@@ -443,7 +720,7 @@ def __str__(self) -> str:
return "elementary verification"
-class LocalVerificationStrategy(TileScopeVerificationStrategy):
+class LocalVerificationStrategy(BasisAwareVerificationStrategy):
"""
The local verified strategy.
@@ -451,9 +728,15 @@ class LocalVerificationStrategy(TileScopeVerificationStrategy):
localized, i.e. in a single cell and the tiling is not 1x1.
"""
- def __init__(self, ignore_parent: bool = False, no_factors: bool = False):
+ def __init__(
+ self,
+ basis: Optional[Iterable[Perm]] = None,
+ symmetry: bool = False,
+ ignore_parent: bool = False,
+ no_factors: bool = False,
+ ):
self.no_factors = no_factors
- super().__init__(ignore_parent=ignore_parent)
+ super().__init__(basis, symmetry, ignore_parent)
def pack(self, comb_class: Tiling) -> StrategyPack:
try:
@@ -461,24 +744,20 @@ def pack(self, comb_class: Tiling) -> StrategyPack:
except StrategyDoesNotApply:
pass
if self.no_factors:
- raise InvalidOperationError("Cannot get a simpler specification")
- if (
- any(isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions)
- and len(comb_class.find_factors()) == 1
- ):
raise InvalidOperationError(
- "Can't find generating function with component assumption."
+ f"Cannot get a simpler specification for\n{comb_class}"
)
return StrategyPack(
- initial_strats=[FactorFactory()],
+ initial_strats=[FactorFactory(), DetectComponentsStrategy()],
inferral_strats=[],
expansion_strats=[],
ver_strats=[
BasicVerificationStrategy(),
- OneByOneVerificationStrategy(),
+ OneByOneVerificationStrategy(self.basis, self._symmetry),
+ ComponentVerificationStrategy(),
InsertionEncodingVerificationStrategy(),
MonotoneTreeVerificationStrategy(no_factors=True),
- LocalVerificationStrategy(no_factors=True),
+ LocalVerificationStrategy(self.basis, self._symmetry, no_factors=True),
],
name="factor pack",
)
@@ -488,10 +767,17 @@ def verified(self, comb_class: Tiling) -> bool:
comb_class.dimensions != (1, 1)
and (not self.no_factors or len(comb_class.find_factors()) == 1)
and LocalEnumeration(comb_class).verified()
+ and all(
+ not isinstance(ass, ComponentAssumption)
+ or (
+ len(ass.gps) == 1
+ and comb_class.only_cell_in_row_and_col(list(ass.cells)[0])
+ )
+ for ass in comb_class.assumptions
+ )
)
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling is locally enumerable"
@classmethod
@@ -500,7 +786,7 @@ def from_dict(cls, d: dict) -> "LocalVerificationStrategy":
def get_genf(
self, comb_class: Tiling, funcs: Optional[Dict[Tiling, Function]] = None
- ) -> Expr:
+ ) -> Any:
if not self.verified(comb_class):
raise StrategyDoesNotApply("tiling not locally verified")
if len(comb_class.obstructions) == 1 and comb_class.obstructions[0] in (
@@ -513,8 +799,7 @@ def get_genf(
except InvalidOperationError:
return LocalEnumeration(comb_class).get_genf(funcs=funcs)
- @staticmethod
- def get_terms(comb_class: Tiling, n: int) -> Terms:
+ def get_terms(self, comb_class: Tiling, n: int) -> Terms:
raise NotImplementedError(
"Not implemented method to count objects for locally verified tilings"
)
@@ -546,18 +831,18 @@ def __init__(self, ignore_parent: bool = False):
super().__init__(ignore_parent=ignore_parent)
def pack(self, comb_class: Tiling) -> StrategyPack:
- if any(isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions):
- raise InvalidOperationError(
- "Can't find generating function with component assumption."
- )
# pylint: disable=import-outside-toplevel
from tilings.strategy_pack import TileScopePack
if self.has_rightmost_insertion_encoding(comb_class):
- return TileScopePack.regular_insertion_encoding(2)
- if self.has_topmost_insertion_encoding(comb_class):
- return TileScopePack.regular_insertion_encoding(3)
- raise StrategyDoesNotApply("tiling does not has a regular insertion encoding")
+ pack = TileScopePack.regular_insertion_encoding(2)
+ elif self.has_topmost_insertion_encoding(comb_class):
+ pack = TileScopePack.regular_insertion_encoding(3)
+ else:
+ raise StrategyDoesNotApply(
+ "tiling does not has a regular insertion encoding"
+ )
+ return pack.add_initial(DetectComponentsStrategy())
@staticmethod
def has_rightmost_insertion_encoding(tiling: Tiling) -> bool:
@@ -578,16 +863,14 @@ def verified(self, comb_class: Tiling) -> bool:
comb_class
) or self.has_topmost_insertion_encoding(comb_class)
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling has a regular insertion encoding"
@classmethod
def from_dict(cls, d: dict) -> "InsertionEncodingVerificationStrategy":
return cls(**d)
- @staticmethod
- def get_terms(comb_class: Tiling, n: int) -> Terms:
+ def get_terms(self, comb_class: Tiling, n: int) -> Terms:
raise NotImplementedError(
"Not implemented method to count objects for insertion encoding "
"verified tilings"
@@ -622,10 +905,6 @@ def __init__(self, ignore_parent: bool = False, no_factors: bool = True):
super().__init__(ignore_parent=ignore_parent)
def pack(self, comb_class: Tiling) -> StrategyPack:
- if any(isinstance(ass, ComponentAssumption) for ass in comb_class.assumptions):
- raise InvalidOperationError(
- "Can't find generating function with component assumption."
- )
try:
return InsertionEncodingVerificationStrategy().pack(comb_class)
except StrategyDoesNotApply:
@@ -650,8 +929,7 @@ def verified(self, comb_class: Tiling) -> bool:
not self.no_factors or len(comb_class.find_factors()) == 1
) and MonotoneTreeEnumeration(comb_class).verified()
- @staticmethod
- def formal_step() -> str:
+ def formal_step(self) -> str:
return "tiling is a monotone tree"
@classmethod
@@ -660,7 +938,7 @@ def from_dict(cls, d: dict) -> "MonotoneTreeVerificationStrategy":
def get_genf(
self, comb_class: Tiling, funcs: Optional[Dict[Tiling, Function]] = None
- ) -> Expr:
+ ) -> Any:
if not self.verified(comb_class):
raise StrategyDoesNotApply("tiling not locally verified")
try:
@@ -668,8 +946,7 @@ def get_genf(
except InvalidOperationError:
return MonotoneTreeEnumeration(comb_class).get_genf(funcs=funcs)
- @staticmethod
- def get_terms(comb_class: Tiling, n: int) -> Terms:
+ def get_terms(self, comb_class: Tiling, n: int) -> Terms:
raise NotImplementedError(
"Not implemented method to count objects for monotone tree "
"verified tilings"
diff --git a/tilings/strategy_pack.py b/tilings/strategy_pack.py
index 35b9e64a..c1bf9112 100644
--- a/tilings/strategy_pack.py
+++ b/tilings/strategy_pack.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Iterable, List, Optional, Union
+from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple, Union
from logzero import logger
@@ -32,14 +32,23 @@ def add_basis(self, basis: Iterable[Perm]) -> "TileScopePack":
basis = tuple(basis)
symmetry = bool(self.symmetries)
- def replace_list(strats):
+ def replace_list(
+ strats: Tuple[Union[AbstractStrategy, StrategyFactory], ...]
+ ) -> List[Union[AbstractStrategy, StrategyFactory]]:
"""Return a new list with the replaced 1x1 strat."""
- res = []
+ res: List[Union[AbstractStrategy, StrategyFactory]] = []
for strategy in strats:
if isinstance(strategy, BasisAwareVerificationStrategy):
if strategy.basis:
logger.warning("Basis changed in %s", strategy)
res.append(strategy.change_basis(basis, symmetry))
+ elif isinstance(strategy, strat.SymmetriesFactory):
+ if strategy.basis:
+ logger.warning("Basis changed in %s", strategy)
+ res.append(strategy.change_basis(basis))
+ elif hasattr(strategy, "change_basis"):
+ logger.warning("Basis changed in %s", strategy)
+ res.append(strategy.change_basis(basis))
else:
res.append(strategy)
return res
@@ -50,7 +59,7 @@ def replace_list(strats):
initial_strats=replace_list(self.initial_strats),
expansion_strats=list(map(replace_list, self.expansion_strats)),
name=self.name,
- symmetries=self.symmetries,
+ symmetries=replace_list(self.symmetries),
iterative=self.iterative,
)
@@ -64,11 +73,13 @@ def setup_subclass_verification(self, start_tiling: "Tiling") -> "TileScopePack"
has length strictly smaller than the maximum length cell basis element.
"""
- def replace_list(strats):
+ def replace_list(
+ strats: Tuple[Union[AbstractStrategy, StrategyFactory], ...]
+ ) -> List[Union[AbstractStrategy, StrategyFactory]]:
"""
Find subclass verification and alter its perms_to_check variable.
"""
- res = []
+ res: List[Union[AbstractStrategy, StrategyFactory]] = []
for strategy in strats:
if isinstance(strategy, strat.SubclassVerificationFactory):
printed_log = False
@@ -95,6 +106,7 @@ def replace_list(strats):
)
printed_log = True
if not printed_log:
+ assert strategy.perms_to_check is not None
logger.info(
"SubclassVerification set up to check the subclasses: "
"Av(%s)",
@@ -118,28 +130,50 @@ def make_tracked(self):
"""Make a fusion pack tracked."""
def replace_list(strats):
- """Return a new list with the replaced fusion strat."""
+ """Return a new list with strats tracked."""
res = []
for strategy in strats:
- if isinstance(strategy, strat.FusionFactory):
- res.append(strategy.make_tracked())
- else:
- res.append(strategy)
+ d = strategy.to_jsonable()
+ if not d.get("tracked", True):
+ d["tracked"] = True
+ strategy = AbstractStrategy.from_dict(d)
+ res.append(strategy)
return res
- return (
- self.__class__(
- ver_strats=replace_list(self.ver_strats),
- inferral_strats=replace_list(self.inferral_strats),
- initial_strats=replace_list(self.initial_strats),
- expansion_strats=list(map(replace_list, self.expansion_strats)),
- name=self.name,
- symmetries=self.symmetries,
- iterative=self.iterative,
- )
- .add_initial(strat.AddAssumptionFactory(), apply_first=True)
- .add_initial(strat.RearrangeAssumptionFactory(), apply_first=True)
+ pack = self.__class__(
+ ver_strats=replace_list(self.ver_strats),
+ inferral_strats=replace_list(self.inferral_strats),
+ initial_strats=replace_list(self.initial_strats),
+ expansion_strats=list(map(replace_list, self.expansion_strats)),
+ name=self.name.replace("untracked", "tracked"),
+ symmetries=self.symmetries,
+ iterative=self.iterative,
)
+ if all(
+ not isinstance(strategy, strat.AddAssumptionFactory) for strategy in pack
+ ):
+ pack = pack.add_initial(strat.AddAssumptionFactory(), apply_first=True)
+ if all(
+ not isinstance(strategy, strat.RearrangeAssumptionFactory)
+ for strategy in pack
+ ):
+ pack = pack.add_initial(
+ strat.RearrangeAssumptionFactory(), apply_first=True
+ )
+ if strat.ComponentFusionFactory() in pack:
+ if all(
+ not isinstance(strategy, strat.DetectComponentsStrategy)
+ for strategy in pack
+ ):
+ pack = pack.add_initial(
+ strat.DetectComponentsStrategy(ignore_parent=True)
+ )
+ if all(
+ not isinstance(strategy, strat.ComponentVerificationStrategy)
+ for strategy in pack
+ ):
+ pack = pack.add_verification(strat.ComponentVerificationStrategy())
+ return pack
def make_fusion(
self,
@@ -174,6 +208,7 @@ def make_fusion(
pack = pack.add_initial(
strat.DetectComponentsStrategy(ignore_parent=True), apply_first=True
)
+ pack = pack.add_verification(strat.ComponentVerificationStrategy())
pack = pack.add_initial(
strat.RearrangeAssumptionFactory(), apply_first=True
)
@@ -186,8 +221,7 @@ def make_interleaving(
Return a new pack where the factor strategy is replaced with an
interleaving factor strategy.
- If unions is set to True it will overwrite unions on the strategy, and
- also pass the argument to AddInterleavingAssumption method.
+ If unions is set to True it will overwrite unions on the strategy.
"""
def replace_list(strats):
@@ -219,9 +253,6 @@ def replace_list(strats):
)
if tracked:
- pack = pack.add_initial(
- strat.AddInterleavingAssumptionFactory(unions=unions), apply_first=True
- )
pack = pack.add_initial(strat.AddAssumptionFactory(), apply_first=True)
return pack
@@ -257,10 +288,158 @@ def add_all_symmetry(self) -> "TileScopePack":
raise ValueError("Symmetries already turned on.")
return super().add_symmetry(strat.SymmetriesFactory(), "symmetries")
+ def kitchen_sinkify( # pylint: disable=R0912
+ self,
+ short_obs_len: int,
+ obs_inferral_len: int,
+ tracked: bool,
+ level: int,
+ ) -> "TileScopePack":
+ """
+ Create a new pack with the following added:
+ Short Obs verification (unless short_obs_len = 0)
+ No Root Cell verification
+ Database verification
+ Deflation
+ Point and/or Assumption Jumping
+ Generalized Monotone Sliding
+ Free Cell Reduction
+ Requirement corroboration
+ Obstruction Inferral (unless obs_inferral_len = 0)
+ Symmetries
+ Point Pointing
+ Unfusion
+ Targeted Row/Col Placements when fusable
+ Relax assumptions
+ Will be made tracked or not, depending on preference.
+ Note that nothing is done with positive / point corroboration, requirement
+ corroboration, or database verification.
+
+ Different stratgies will be added at different levels
+ Level 1: short obs, no root cell, database verification, symmetries,
+ obs inferral, interleaving factor without unions
+ Level 2: deflation, point/assumption jumping, sliding, free cell reduction,
+ req corrob, targeted row/col placements, relax assumptions,
+ interleaving factor with unions
+ Level 3: unfusion 1,1
+ Level 4: unfusion 2,2, pointing mc=4, assumption mc=8
+ Level 5: unfusion 4,4, pointing mc=6, assumption mc=8, requirement pt, mc=4
+ """
+
+ assert level in (
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ ), "Level must be an int between 1 and 5 inclusive"
+
+ ks_pack = self.__class__(
+ ver_strats=self.ver_strats,
+ inferral_strats=self.inferral_strats,
+ initial_strats=self.initial_strats,
+ expansion_strats=self.expansion_strats,
+ name=self.name,
+ symmetries=self.symmetries,
+ iterative=self.iterative,
+ )
+ if short_obs_len > 0:
+ ver_strats: List[CSSstrategy] = [
+ strat.ShortObstructionVerificationStrategy(short_obs_len)
+ ]
+ else:
+ ver_strats = []
+
+ ver_strats += [
+ strat.NoRootCellVerificationStrategy(),
+ strat.DatabaseVerificationStrategy(),
+ ]
+
+ for strategy in ver_strats:
+ try:
+ ks_pack = ks_pack.add_verification(strategy)
+ except ValueError:
+ pass
+
+ if level >= 2:
+ initial_strats: List[CSSstrategy] = [
+ strat.DeflationFactory(tracked),
+ strat.AssumptionAndPointJumpingFactory(),
+ strat.MonotoneSlidingFactory(),
+ strat.CellReductionFactory(tracked),
+ strat.RequirementCorroborationFactory(),
+ strat.RelaxAssumptionFactory(),
+ ]
+ for strategy in initial_strats:
+ try:
+ ks_pack = ks_pack.add_initial(strategy)
+ except ValueError:
+ pass
+
+ if obs_inferral_len > 0:
+ inf_strats: List[CSSstrategy] = [
+ strat.ObstructionInferralFactory(obs_inferral_len)
+ ]
+ else:
+ inf_strats = []
+ for strategy in inf_strats:
+ try:
+ ks_pack = ks_pack.add_inferral(strategy)
+ except ValueError:
+ pass
+
+ ks_pack = ks_pack.make_interleaving(tracked=tracked, unions=(level > 1))
+
+ try:
+ ks_pack = ks_pack.add_all_symmetry()
+ except ValueError:
+ pass
+
+ if tracked:
+ ks_pack = ks_pack.make_tracked()
+
+ if level == 2:
+ ks_pack.expansion_strats = ks_pack.expansion_strats + (
+ (strat.FusableRowAndColumnPlacementFactory(),),
+ )
+ elif level == 3:
+ ks_pack.expansion_strats = ks_pack.expansion_strats + (
+ (
+ strat.UnfusionFactory(1, 1),
+ strat.FusableRowAndColumnPlacementFactory(),
+ ),
+ )
+ elif level == 4:
+ ks_pack.expansion_strats = ks_pack.expansion_strats + (
+ (
+ strat.AssumptionPointingFactory(8),
+ strat.PointingStrategy(4),
+ strat.UnfusionFactory(2, 2),
+ strat.FusableRowAndColumnPlacementFactory(),
+ ),
+ )
+ elif level == 5:
+ ks_pack.expansion_strats = ks_pack.expansion_strats + (
+ (
+ strat.AssumptionPointingFactory(8),
+ strat.RequirementPointingFactory(4),
+ strat.PointingStrategy(6),
+ strat.UnfusionFactory(4, 4),
+ strat.FusableRowAndColumnPlacementFactory(),
+ ),
+ )
+
+ ks_pack.name += f"_kitchen_sink_level_{level}"
+
+ return ks_pack
+
# Creation of the base pack
@classmethod
def all_the_strategies(cls, length: int = 1) -> "TileScopePack":
- initial_strats: List[CSSstrategy] = [strat.FactorFactory()]
+ initial_strats: List[CSSstrategy] = [
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ ]
if length > 1:
initial_strats.append(strat.RequirementCorroborationFactory())
@@ -299,14 +478,16 @@ def pattern_placements(
)
expansion_strats: List[CSSstrategy] = [
- strat.FactorFactory(unions=True),
strat.CellInsertionFactory(maxreqlen=length),
]
if length > 1:
expansion_strats.append(strat.RequirementCorroborationFactory())
return TileScopePack(
- initial_strats=[strat.PatternPlacementFactory(partial=partial)],
+ initial_strats=[
+ strat.FactorFactory(unions=True, ignore_parent=False),
+ strat.PointCorroborationFactory(),
+ ],
ver_strats=[
strat.BasicVerificationStrategy(),
strat.InsertionEncodingVerificationStrategy(),
@@ -317,7 +498,10 @@ def pattern_placements(
strat.RowColumnSeparationStrategy(),
strat.ObstructionTransitivityFactory(),
],
- expansion_strats=[expansion_strats],
+ expansion_strats=[
+ [strat.PatternPlacementFactory(partial=partial)],
+ expansion_strats,
+ ],
name=name,
)
@@ -327,13 +511,16 @@ def point_placements(
) -> "TileScopePack":
name = "".join(
[
- "length_{length}_" if length > 1 else "",
+ f"length_{length}_" if length > 1 else "",
"partial_" if partial else "",
"point_placements",
]
)
- initial_strats: List[CSSstrategy] = [strat.FactorFactory()]
+ initial_strats: List[CSSstrategy] = [
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ ]
if length > 1:
initial_strats.append(strat.RequirementCorroborationFactory())
@@ -440,7 +627,10 @@ def row_and_col_placements(
if partial:
expansion_strats.append([strat.PatternPlacementFactory(point_only=True)])
return TileScopePack(
- initial_strats=[strat.FactorFactory()],
+ initial_strats=[
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ ],
ver_strats=[
strat.BasicVerificationStrategy(),
strat.InsertionEncodingVerificationStrategy(),
@@ -503,6 +693,7 @@ def only_root_placements(
initial_strats=[
strat.RootInsertionFactory(maxreqlen=length, max_num_req=max_num_req),
strat.FactorFactory(unions=True, ignore_parent=False, workable=False),
+ strat.PointCorroborationFactory(),
],
ver_strats=[
strat.BasicVerificationStrategy(),
@@ -524,13 +715,16 @@ def requirement_placements(
) -> "TileScopePack":
name = "".join(
[
- "length_{length}_" if length != 2 else "",
+ f"length_{length}_" if length != 2 else "",
"partial_" if partial else "",
"requirement_placements",
]
)
- initial_strats: List[CSSstrategy] = [strat.FactorFactory()]
+ initial_strats: List[CSSstrategy] = [
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ ]
if length > 1:
initial_strats.append(strat.RequirementCorroborationFactory())
@@ -555,6 +749,64 @@ def requirement_placements(
name=name,
)
+ @classmethod
+ def subobstruction_placements(cls, partial: bool = False) -> "TileScopePack":
+ name = "partial_" if partial else ""
+ name += "subobstruction_placements"
+ return TileScopePack(
+ initial_strats=[
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ strat.RequirementCorroborationFactory(),
+ ],
+ ver_strats=[
+ strat.BasicVerificationStrategy(),
+ strat.InsertionEncodingVerificationStrategy(),
+ strat.OneByOneVerificationStrategy(),
+ strat.LocallyFactorableVerificationStrategy(),
+ ],
+ inferral_strats=[
+ strat.RowColumnSeparationStrategy(),
+ strat.ObstructionTransitivityFactory(),
+ ],
+ expansion_strats=[
+ [
+ strat.SubobstructionInsertionFactory(),
+ strat.PatternPlacementFactory(partial=partial),
+ ],
+ ],
+ name=name,
+ )
+
+ @classmethod
+ def basis_pattern_insertions(cls, partial: bool = False) -> "TileScopePack":
+ name = "partial_" if partial else ""
+ name += "basis_pattern_insertions"
+ return TileScopePack(
+ initial_strats=[
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ strat.RequirementCorroborationFactory(),
+ ],
+ ver_strats=[
+ strat.BasicVerificationStrategy(),
+ strat.InsertionEncodingVerificationStrategy(),
+ strat.OneByOneVerificationStrategy(),
+ strat.LocallyFactorableVerificationStrategy(),
+ ],
+ inferral_strats=[
+ strat.RowColumnSeparationStrategy(),
+ strat.ObstructionTransitivityFactory(),
+ ],
+ expansion_strats=[
+ [
+ strat.BasisPatternInsertionFactory(),
+ strat.PatternPlacementFactory(partial=partial),
+ ],
+ ],
+ name=name,
+ )
+
@classmethod
def point_and_row_and_col_placements(
cls,
@@ -570,7 +822,7 @@ def point_and_row_and_col_placements(
both = place_col and place_row
name = "".join(
[
- "length_{length}_" if length > 1 else "",
+ f"length_{length}_" if length > 1 else "",
"partial_" if partial else "",
"point_and_",
"row" if not col_only else "",
@@ -583,7 +835,10 @@ def point_and_row_and_col_placements(
place_row=place_row, place_col=place_col, partial=partial
)
- initial_strats: List[CSSstrategy] = [strat.FactorFactory()]
+ initial_strats: List[CSSstrategy] = [
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ ]
if length > 1:
initial_strats.append(strat.RequirementCorroborationFactory())
@@ -609,6 +864,63 @@ def point_and_row_and_col_placements(
name=name,
)
+ @classmethod
+ def requirement_and_row_and_col_placements(
+ cls,
+ length: int = 1,
+ row_only: bool = False,
+ col_only: bool = False,
+ partial: bool = False,
+ ) -> "TileScopePack":
+ if row_only and col_only:
+ raise ValueError("Can't be row and col only.")
+ place_row = not col_only
+ place_col = not row_only
+ both = place_col and place_row
+ name = "".join(
+ [
+ f"length_{length}_" if length > 1 else "",
+ "partial_" if partial else "",
+ "requirement_and_",
+ "row" if not col_only else "",
+ "_and_" if both else "",
+ "col" if not row_only else "",
+ "_placements",
+ ]
+ )
+ rowcol_strat = strat.RowAndColumnPlacementFactory(
+ place_row=place_row, place_col=place_col, partial=partial
+ )
+
+ initial_strats: List[CSSstrategy] = [
+ strat.FactorFactory(),
+ strat.PointCorroborationFactory(),
+ ]
+ if length > 1:
+ initial_strats.append(strat.RequirementCorroborationFactory())
+
+ return TileScopePack(
+ initial_strats=initial_strats,
+ ver_strats=[
+ strat.BasicVerificationStrategy(),
+ strat.InsertionEncodingVerificationStrategy(),
+ strat.OneByOneVerificationStrategy(),
+ strat.LocallyFactorableVerificationStrategy(),
+ ],
+ inferral_strats=[
+ strat.RowColumnSeparationStrategy(),
+ strat.ObstructionTransitivityFactory(),
+ ],
+ expansion_strats=[
+ [
+ strat.RequirementInsertionFactory(maxreqlen=length),
+ strat.PatternPlacementFactory(partial=partial),
+ rowcol_strat,
+ ],
+ ],
+ name=name,
+ )
+
@classmethod
def cell_insertions(cls, length: int):
return TileScopePack(
@@ -620,5 +932,5 @@ def cell_insertions(cls, length: int):
],
inferral_strats=[],
expansion_strats=[[strat.CellInsertionFactory(maxreqlen=length)]],
- name="length_{length}_cell_insertions",
+ name=f"length_{length}_cell_insertions",
)
diff --git a/tilings/tilescope.py b/tilings/tilescope.py
index 330299f3..238563a7 100644
--- a/tilings/tilescope.py
+++ b/tilings/tilescope.py
@@ -1,3 +1,6 @@
+import itertools
+import math
+from array import array
from collections import Counter
from typing import Counter as CounterType
from typing import (
@@ -9,6 +12,7 @@
Optional,
Set,
Tuple,
+ Type,
Union,
cast,
)
@@ -21,15 +25,22 @@
CombinatorialSpecification,
CombinatorialSpecificationSearcher,
)
+from comb_spec_searcher.class_db import ClassDB, ClassKey, Info, Key
from comb_spec_searcher.class_queue import CSSQueue, DefaultQueue, WorkPacket
from comb_spec_searcher.rule_db.abstract import RuleDBAbstract
+from comb_spec_searcher.strategies.rule import AbstractRule
from comb_spec_searcher.typing import CombinatorialClassType, CSSstrategy
from permuta import Basis, Perm
from tilings import GriddedPerm, Tiling
+from tilings.assumptions import TrackingAssumption
from tilings.strategy_pack import TileScopePack
__all__ = ("TileScope", "TileScopePack", "LimitedAssumptionTileScope", "GuidedSearcher")
+Cell = Tuple[int, int]
+TrackedClassAssumption = Tuple[int, Tuple[Cell, ...]]
+TrackedClassDBKey = Tuple[int, Tuple[TrackedClassAssumption, ...]]
+
class TileScope(CombinatorialSpecificationSearcher):
"""
@@ -42,6 +53,8 @@ def __init__(
start_class: Union[str, Iterable[Perm], Tiling],
strategy_pack: TileScopePack,
ruledb: Optional[RuleDBAbstract] = None,
+ classdb: Optional[ClassDB] = None,
+ classqueue: Optional[CSSQueue] = None,
expand_verified: bool = False,
debug: bool = False,
) -> None:
@@ -74,7 +87,9 @@ def __init__(
super().__init__(
start_class=start_tiling,
strategy_pack=strategy_pack,
+ classdb=classdb,
ruledb=ruledb,
+ classqueue=classqueue,
expand_verified=expand_verified,
debug=debug,
)
@@ -91,35 +106,40 @@ def __init__(
start_class: Union[str, Iterable[Perm], Tiling],
strategy_pack: TileScopePack,
max_assumptions: int,
+ ignore_full_tiling_assumptions: bool = False,
**kwargs,
) -> None:
- super().__init__(start_class, strategy_pack, **kwargs)
self.max_assumptions = max_assumptions
+ super().__init__(
+ start_class,
+ strategy_pack,
+ classdb=TrackedClassDB(),
+ **kwargs,
+ )
+ self.ignore_full_tiling_assumptions = ignore_full_tiling_assumptions
- def _expand(
- self,
- comb_class: CombinatorialClassType,
- label: int,
- strategies: Tuple[CSSstrategy, ...],
- inferral: bool,
- ) -> None:
+ def _rules_from_strategy( # type: ignore
+ self, comb_class: CombinatorialClassType, strategy: CSSstrategy
+ ) -> Iterator[AbstractRule]:
"""
- Will expand the combinatorial class with given label using the given
- strategies, but only add rules whose children all satisfy the max_assumptions
- requirement.
+ Yield all the rules given by a strategy/strategy factory whose children all
+ satisfy the max_assumptions constraint.
"""
- if inferral:
- self._inferral_expand(comb_class, label, strategies)
- else:
- for strategy_generator in strategies:
- for start_label, end_labels, rule in self._expand_class_with_strategy(
- comb_class, strategy_generator, label
- ):
- if all(
- len(child.assumptions) <= self.max_assumptions
- for child in rule.children
- ):
- self.add_rule(start_label, end_labels, rule)
+ # pylint: disable=arguments-differ
+ def num_child_assumptions(child: Tiling) -> int:
+ return sum(
+ 1
+ for ass in child.assumptions
+ if (not self.ignore_full_tiling_assumptions)
+ or len(ass.gps) != len(child.active_cells)
+ )
+
+ for rule in super()._rules_from_strategy(comb_class, strategy):
+ if all(
+ num_child_assumptions(child) <= self.max_assumptions
+ for child in rule.children
+ ):
+ yield rule
class GuidedSearcher(TileScope):
@@ -128,16 +148,21 @@ def __init__(
tilings: Iterable[Tiling],
basis: Tiling,
pack: TileScopePack,
- *args,
**kwargs,
):
self.tilings = frozenset(t.remove_assumptions() for t in tilings)
- super().__init__(basis, pack, *args, **kwargs)
+ super().__init__(
+ basis,
+ pack,
+ classdb=TrackedClassDB(),
+ **kwargs,
+ )
for t in self.tilings:
class_label = self.classdb.get_label(t)
is_empty = self.classdb.is_empty(t, class_label)
if not is_empty:
self.classqueue.add(class_label)
+ self._symmetry_expand(t, class_label)
def _expand(
self,
@@ -150,6 +175,18 @@ def _expand(
return
return super()._expand(comb_class, label, strategies, inferral)
+ def _symmetry_expand(self, comb_class: CombinatorialClassType, label: int) -> None:
+ sym_labels = set([label])
+ for strategy_generator in self.symmetries:
+ for start_label, end_labels, rule in self._expand_class_with_strategy(
+ comb_class, strategy_generator, label=label
+ ):
+ sym_label = end_labels[0]
+ self.ruledb.add(start_label, (sym_label,), rule)
+ self.classqueue.add(sym_label)
+ sym_labels.add(sym_label)
+ self.symmetry_expanded.update(sym_labels)
+
@classmethod
def from_spec(
cls, specification: CombinatorialSpecification, pack: TileScopePack
@@ -160,7 +197,7 @@ def from_spec(
@classmethod
def from_uri(cls, URI: str) -> "GuidedSearcher":
- response = requests.get(URI)
+ response = requests.get(URI, timeout=10)
spec = CombinatorialSpecification.from_dict(response.json()["specification"])
pack = TileScopePack.from_dict(response.json()["pack"]).make_tracked()
return cls.from_spec(spec, pack)
@@ -188,13 +225,14 @@ def __init__(
**kwargs,
) -> None:
super().__init__(
- start_class, strategy_pack, max_assumptions=max_assumptions, **kwargs
+ start_class,
+ strategy_pack,
+ max_assumptions=max_assumptions,
+ classqueue=TrackedQueue(
+ cast(TileScopePack, strategy_pack), self, delay_next
+ ),
+ **kwargs,
)
- # reset to the trackedqueue!
- self.classqueue = cast(
- DefaultQueue, TrackedQueue(strategy_pack, self, delay_next)
- ) # TODO: make CSS accept a CSSQueue as a kwarg
- self.classqueue.add(self.start_label)
class TrackedDefaultQueue(DefaultQueue):
@@ -331,11 +369,12 @@ def status(self) -> str:
f"Queue {idx}" for idx in range(len(self.queues) - 1)
)
underlying = ("underlying",) + tuple(
- self._underlyng_labels_per_level[level] for level in range(len(self.queues))
+ str(self._underlyng_labels_per_level[level])
+ for level in range(len(self.queues))
)
table.append(underlying)
all_labels = ("all labels",) + tuple(
- self._all_labels_per_level[level] for level in range(len(self.queues))
+ str(self._all_labels_per_level[level]) for level in range(len(self.queues))
)
table.append(all_labels)
table = [headers] + table
@@ -361,3 +400,241 @@ def __next__(self) -> WorkPacket:
return next(queue)
except StopIteration:
continue
+ raise StopIteration("No elements in queue")
+
+
+class TrackedClassDB(ClassDB[Tiling]):
+ def __init__(self) -> None:
+ super().__init__(Tiling)
+ self.classdb = ClassDB(Tiling)
+ self.label_to_tilings: List[bytes] = []
+ self.tilings_to_label: Dict[bytes, int] = {}
+ self.assumption_type_to_int: Dict[Type[TrackingAssumption], int] = {}
+ self.int_to_assumption_type: List[Type[TrackingAssumption]] = []
+
+ def __iter__(self) -> Iterator[int]:
+ for key in self.label_to_info:
+ yield key
+
+ def __contains__(self, key: Key) -> bool:
+ if isinstance(key, Tiling):
+ actual_key = self.tiling_to_key(key)
+ compressed_key = self._compress_key(actual_key)
+ return self.tilings_to_label.get(compressed_key) is not None
+ if isinstance(key, int):
+ return 0 <= key < len(self.label_to_tilings)
+ raise ValueError("Invalid key")
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, TrackedClassDB):
+ return NotImplemented
+ return bool(
+ self.classdb == other.classdb
+ and self.label_to_tilings == other.label_to_tilings
+ and self.tilings_to_label == other.tilings_to_label
+ )
+
+ def tiling_to_key(self, tiling: Tiling) -> TrackedClassDBKey:
+ """
+ Converts a tiling to its corresponding key.
+ """
+ underlying_label = self.classdb.get_label(tiling.remove_assumptions())
+ assumption_keys = tuple(
+ self.assumption_to_key(ass) for ass in tiling.assumptions
+ )
+ return (underlying_label, assumption_keys)
+
+ def assumption_to_key(self, ass: TrackingAssumption) -> TrackedClassAssumption:
+ """
+ Determines the type of the assumption and retrieves the int representing
+ that type from the appropriate class variables, and then apprends the cells.
+ """
+ try:
+ ass_type_int = self.assumption_type_to_int[type(ass)]
+ except KeyError:
+ ass_type_int = len(self.int_to_assumption_type)
+ assert ass_type_int < 256
+ self.int_to_assumption_type.append(type(ass))
+ self.assumption_type_to_int[type(ass)] = ass_type_int
+ return (ass_type_int, ass.get_cells())
+
+ def key_to_tiling(self, key: TrackedClassDBKey) -> Tiling:
+ """
+ Converts a key back to a Tiling.
+ """
+ return self.classdb.get_class(key[0]).add_assumptions(
+ (
+ self.int_to_assumption_type[ass_key[0]].from_cells(ass_key[1])
+ for ass_key in key[1]
+ ),
+ clean=False,
+ )
+
+ @staticmethod
+ def _compress_key(key: TrackedClassDBKey) -> bytes:
+ # Assumes there are fewer than 256 assumptions
+ # Assumes every assumption covers fewer than 256 cells
+ # Assumes the positions in an assumption have value < 256
+
+ def int_to_bytes(n: int) -> List[int]:
+ """
+ Converts an int to a list of ints all in [0 .. 255] ready for
+ byte compression. First entry is the number of bytes needed (assumes < 256),
+ remaining entries the bytes composing the int from lowest byte up to largest
+ byte.
+ """
+ bytes_needed = max(math.ceil(n.bit_length() / 8), 1)
+ result: List[int] = [bytes_needed]
+ while n >= 2**8:
+ result.append(n & 0xFF)
+ n = n >> 8
+ result.append(n)
+ return result
+
+ def _compress_assumption(ass_key: TrackedClassAssumption) -> List[int]:
+ type_int, cells = ass_key
+ assert type_int < 256
+ assert len(cells) < 256
+ assert all(cell[0] < 256 and cell[1] < 256 for cell in cells)
+
+ result = [type_int]
+ result.append(len(cells))
+ result.extend(itertools.chain(*cells))
+ return result
+
+ result: List[int] = int_to_bytes(key[0])
+ result.extend(
+ itertools.chain.from_iterable(
+ _compress_assumption(ass_key) for ass_key in key[1]
+ )
+ )
+ compressed_key = array("B", result).tobytes()
+ return compressed_key
+
+ @staticmethod
+ def _decompress_key(compressed_key: bytes) -> TrackedClassDBKey:
+ def int_from_bytes(n: array) -> int:
+ """
+ Converts a list of ints to a single int assuming the first entry is the
+ lowest byte and so on.
+ """
+ result = n[0]
+ for idx in range(1, len(n)):
+ result |= n[idx] << (8 * idx)
+ return cast(int, result)
+
+ def _decompress_tuple_of_cells(
+ compressed_cells: array,
+ ) -> Tuple[Cell, ...]:
+ """
+ compressed_cells is a list of 2*i bytes, each of which is a coordinates
+ """
+ vals = iter(compressed_cells)
+ return tuple(
+ (next(vals), next(vals)) for _ in range(len(compressed_cells) // 2)
+ )
+
+ vals = array("B", compressed_key)
+ offset = 0
+
+ num_bytes_int = vals[offset]
+ offset += 1
+ label = int_from_bytes(vals[offset : offset + num_bytes_int])
+ offset += num_bytes_int
+
+ tuples_of_cells = []
+ while offset < len(vals):
+ type_int, num_cells = vals[offset : offset + 2]
+ offset += 2
+ tuples_of_cells.append(
+ (
+ type_int,
+ _decompress_tuple_of_cells(vals[offset : offset + 2 * num_cells]),
+ )
+ )
+ offset += 2 * num_cells
+
+ return (label, tuple(tuples_of_cells))
+
+ def add(self, comb_class: ClassKey, compressed: bool = False) -> None:
+ """
+ Adds a Tiling to the classdb
+ """
+ if compressed:
+ raise NotImplementedError
+ if isinstance(comb_class, Tiling):
+ key = self.tiling_to_key(comb_class)
+ compressed_key = self._compress_key(key)
+ if compressed_key not in self.tilings_to_label:
+ self.label_to_tilings.append(compressed_key)
+ self.tilings_to_label[compressed_key] = len(self.tilings_to_label)
+
+ def _get_info(self, key: Key) -> Info:
+ """
+ Return the "Info" object corresponding to the key, which is
+ either a Tiling or an integer
+ """
+ # pylint: disable=protected-access
+ if isinstance(key, Tiling):
+ actual_key = self.tiling_to_key(key)
+ compressed_key = self._compress_key(actual_key)
+ if compressed_key not in self.tilings_to_label:
+ self.add(key)
+ info: Optional[Info] = self.classdb._get_info(actual_key[0])
+ if info is None:
+ raise ValueError("Invalid key")
+ info = Info(
+ key,
+ self.tilings_to_label[compressed_key],
+ info.empty,
+ )
+ elif isinstance(key, int):
+ if not 0 <= key < len(self.label_to_tilings):
+ raise KeyError("Key not in ClassDB")
+ tiling_key = self._decompress_key(self.label_to_tilings[key])
+ info = self.classdb.label_to_info.get(tiling_key[0])
+ if info is None:
+ raise ValueError("Invalid key")
+ info = Info(
+ self.key_to_tiling(tiling_key),
+ key,
+ info.empty,
+ )
+ else:
+ raise TypeError()
+ return info
+
+ def get_class(self, key: Key) -> Tiling:
+ """
+ Return combinatorial class of key.
+ """
+ info = self._get_info(key)
+ return cast(Tiling, info.comb_class)
+
+ def is_empty(self, comb_class: Tiling, label: Optional[int] = None) -> bool:
+ """
+ Return True if combinatorial class is set to be empty, False if not.
+ """
+ return bool(self.classdb.is_empty(comb_class.remove_assumptions()))
+
+ def set_empty(self, key: Key, empty: bool = True) -> None:
+ """
+ Set a class to be empty.
+ """
+ if isinstance(key, int):
+ if 0 <= key < len(self.label_to_tilings):
+ underlying_label, _ = self._decompress_key(self.label_to_tilings[key])
+ if isinstance(key, Tiling):
+ underlying_label = self.classdb.get_label(key.remove_assumptions())
+ self.classdb.set_empty(underlying_label, empty)
+
+ def status(self) -> str:
+ """
+ Return a string with the current status of the run.
+ """
+ status = self.classdb.status()
+ status = status.replace("combinatorial classes", "underlying tilings")
+ tilings = "\n\tTotal number of tilings found is"
+ tilings += f" {len(self.label_to_tilings):,d}"
+ status = status.replace("ClassDB status:", "TrackedClassDB status:" + tilings)
+ return status + "\n"
diff --git a/tilings/tiling.py b/tilings/tiling.py
index b97233d1..9269a162 100644
--- a/tilings/tiling.py
+++ b/tilings/tiling.py
@@ -6,6 +6,7 @@
from itertools import chain, filterfalse, product
from operator import mul, xor
from typing import (
+ Any,
Callable,
Dict,
FrozenSet,
@@ -46,6 +47,7 @@
guess_obstructions,
)
from .assumptions import (
+ ComponentAssumption,
SkewComponentAssumption,
SumComponentAssumption,
TrackingAssumption,
@@ -545,6 +547,10 @@ def from_dict(cls, d: dict) -> "Tiling":
obstructions=obstructions,
requirements=requirements,
assumptions=assumptions,
+ remove_empty_rows_and_cols=False,
+ derive_empty=False,
+ simplify=False,
+ sorted_input=True,
)
# -------------------------------------------------------------
@@ -575,26 +581,31 @@ def insert_cell(self, cell: Cell) -> "Tiling":
def add_obstruction(self, patt: Perm, pos: Iterable[Cell]) -> "Tiling":
"""Returns a new tiling with the obstruction of the pattern
patt with positions pos."""
- return Tiling(
- self._obstructions + (GriddedPerm(patt, pos),),
- self._requirements,
- self._assumptions,
- )
+ return self.add_obstructions((GriddedPerm(patt, pos),))
def add_obstructions(self, gps: Iterable[GriddedPerm]) -> "Tiling":
"""Returns a new tiling with the obstructions added."""
new_obs = tuple(gps)
return Tiling(
- self._obstructions + new_obs, self._requirements, self._assumptions
+ sorted(self._obstructions + new_obs),
+ self._requirements,
+ self._assumptions,
+ sorted_input=True,
+ derive_empty=False,
)
def add_list_requirement(self, req_list: Iterable[GriddedPerm]) -> "Tiling":
"""
Return a new tiling with the requirement list added.
"""
- new_req = tuple(req_list)
+ new_req = tuple(sorted(req_list))
return Tiling(
- self._obstructions, self._requirements + (new_req,), self._assumptions
+ self._obstructions,
+ sorted(self._requirements + (new_req,)),
+ self._assumptions,
+ sorted_input=True,
+ already_minimized_obs=True,
+ derive_empty=False,
)
def add_requirement(self, patt: Perm, pos: Iterable[Cell]) -> "Tiling":
@@ -636,7 +647,9 @@ def add_assumption(self, assumption: TrackingAssumption) -> "Tiling":
"""Returns a new tiling with the added assumption."""
return self.add_assumptions((assumption,))
- def add_assumptions(self, assumptions: Iterable[TrackingAssumption]) -> "Tiling":
+ def add_assumptions(
+ self, assumptions: Iterable[TrackingAssumption], clean: bool = True
+ ) -> "Tiling":
"""Returns a new tiling with the added assumptions."""
tiling = Tiling(
self._obstructions,
@@ -647,7 +660,8 @@ def add_assumptions(self, assumptions: Iterable[TrackingAssumption]) -> "Tiling"
simplify=False,
sorted_input=True,
)
- tiling.clean_assumptions()
+ if clean:
+ tiling.clean_assumptions()
return tiling
def remove_assumption(self, assumption: TrackingAssumption):
@@ -670,7 +684,7 @@ def remove_assumption(self, assumption: TrackingAssumption):
tiling.clean_assumptions()
return tiling
- def remove_assumptions(self):
+ def remove_assumptions(self) -> "Tiling":
"""
Return the tiling with all assumptions removed.
"""
@@ -746,7 +760,7 @@ def only_cell_in_row(self, cell: Cell) -> bool:
return sum(1 for (x, y) in self.active_cells if y == cell[1]) == 1
def only_cell_in_row_and_col(self, cell: Cell) -> bool:
- """Checks if the cell is the only active cell in the row."""
+ """Checks if the cell is the only active cell in the row and column."""
return (
sum(1 for (x, y) in self.active_cells if y == cell[1] or x == cell[0]) == 1
)
@@ -897,6 +911,9 @@ def _transform(
ass.__class__(gptransf(gp) for gp in ass.gps)
for ass in self._assumptions
),
+ remove_empty_rows_and_cols=False,
+ derive_empty=False,
+ simplify=False,
)
def reverse(self, regions=False):
@@ -981,8 +998,6 @@ def all_symmetries(self) -> Set["Tiling"]:
symmetries.add(t)
symmetries.add(t.inverse())
t = t.rotate90()
- if t in symmetries:
- break
return symmetries
def column_reverse(self, column: int) -> "Tiling":
@@ -1418,6 +1433,12 @@ def get_minimum_value(self, parameter: str) -> int:
Return the minimum value that can be taken by the parameter.
"""
assumption = self.get_assumption(parameter)
+ if isinstance(assumption, ComponentAssumption):
+ return (
+ 1
+ if any(gp.pos[0] in self.positive_cells for gp in assumption.gps)
+ else 0
+ )
return min(assumption.get_value(gp) for gp in self.minimal_gridded_perms())
def maximum_length_of_minimum_gridded_perm(self) -> int:
@@ -1454,6 +1475,34 @@ def is_finite(self) -> bool:
cell in increasing and cell in decreasing for cell in self.active_cells
)
+ def is_increasing(self) -> bool:
+ """Returns true if all gridded perms are increasing."""
+ separated = self.row_and_column_separation()
+ components = separated.sum_decomposition()
+ if any(len(cells) > 1 for cells in components):
+ return False
+ cells = sorted(cells[0] for cells in components)
+ if any(b < a for a, b in zip(cells[:-1], cells[1:])):
+ return False
+ return all(
+ GriddedPerm.single_cell(Perm((1, 0)), cell) in separated.obstructions
+ for cell in cells
+ )
+
+ def is_decreasing(self) -> bool:
+ """Returns true if all gridded perms are decreasing."""
+ separated = self.row_and_column_separation()
+ components = separated.skew_decomposition()
+ if any(len(cells) > 1 for cells in components):
+ return False
+ cells = sorted(cells[0] for cells in components)
+ if any(b > a for a, b in zip(cells[:-1], cells[1:])):
+ return False
+ return all(
+ GriddedPerm.single_cell(Perm((0, 1)), cell) in separated.obstructions
+ for cell in cells
+ )
+
def objects_of_size(self, n: int, **parameters: int) -> Iterator[GriddedPerm]:
for gp in self.gridded_perms_of_length(n):
if all(
@@ -1467,7 +1516,7 @@ def gridded_perms_of_length(self, length: int) -> Iterator[GriddedPerm]:
if len(gp) == length:
yield gp
- def initial_conditions(self, check: int = 6) -> List[sympy.Expr]:
+ def initial_conditions(self, check: int = 6) -> List[Any]:
"""
Returns a list with the initial conditions to size `check` of the
CombinatorialClass.
@@ -1557,14 +1606,12 @@ def minimum_size_of_object(self) -> int:
return min(len(gp) for gp in self.requirements[0])
return len(next(self.minimal_gridded_perms()))
- def is_point_or_empty(self) -> bool:
- point_or_empty_tiling = Tiling(
- obstructions=(
- GriddedPerm((0, 1), ((0, 0), (0, 0))),
- GriddedPerm((1, 0), ((0, 0), (0, 0))),
- )
+ def is_point_or_empty_cell(self, cell: Cell) -> bool:
+ point_or_empty_obs = (
+ GriddedPerm((0, 1), (cell, cell)),
+ GriddedPerm((1, 0), (cell, cell)),
)
- return self == point_or_empty_tiling
+ return all(ob in self.obstructions for ob in point_or_empty_obs)
def is_empty_cell(self, cell: Cell) -> bool:
"""Check if the cell of the tiling is empty."""
@@ -1772,9 +1819,11 @@ def rec(
res: List[GriddedPerm] = []
rec(cols, patt, pos, used, 0, 0, res)
return Tiling(
- obstructions=list(self.obstructions) + res,
+ obstructions=sorted(list(self.obstructions) + res),
requirements=self.requirements,
assumptions=self.assumptions,
+ sorted_input=True,
+ derive_empty=False,
)
@classmethod
@@ -1787,7 +1836,7 @@ def tiling_from_perm(cls, p: Perm) -> "Tiling":
requirements=[[GriddedPerm((0,), ((i, p[i]),))] for i in range(len(p))]
)
- def get_genf(self, *args, **kwargs) -> sympy.Expr:
+ def get_genf(self, *args, **kwargs) -> Any:
# pylint: disable=import-outside-toplevel
if self.is_empty():
return sympy.sympify(0)
diff --git a/tox.ini b/tox.ini
index 62ccfa16..add7e7d8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,11 +6,11 @@
[tox]
envlist =
flake8, mypy, pylint, black
- py{38,39,310},
- pypy38
+ py{38,39,310,311},
+ pypy{38,39}
[default]
-basepython=python3.8
+basepython=python3.10
[testenv]
description = run test
@@ -18,10 +18,12 @@ basepython =
py38: python3.8
py39: python3.9
py310: python3.10
- pypy38: pypy3
+ py311: python3.11
+ pypy38: pypy3.8
+ pypy39: pypy3.9
deps =
- pytest==6.2.5
- pytest-timeout==2.0.1
+ pytest==7.2.0
+ pytest-timeout==2.1.0
commands = pytest
[pytest]
@@ -35,8 +37,8 @@ description = run flake8 (linter)
basepython = {[default]basepython}
skip_install = True
deps =
- flake8==4.0.1
- flake8-isort==4.1.1
+ flake8==5.0.4
+ flake8-isort==5.0.0
commands =
flake8 --isort-show-traceback tilings tests setup.py
@@ -44,21 +46,21 @@ commands =
description = run pylint (static code analysis)
basepython = {[default]basepython}
deps =
- pylint==2.11.1
+ pylint==2.15.5
commands = pylint tilings
[testenv:mypy]
description = run mypy (static type checker)
basepython = {[default]basepython}
deps =
- mypy==0.910
- types-requests==2.26.0
- types-tabulate==0.8.3
+ mypy==0.990
+ types-requests==2.28.11.4
+ types-tabulate==0.9.0
commands = mypy
[testenv:black]
description = check that comply with autoformating
basepython = {[default]basepython}
deps =
- black==21.10b0
+ black==22.10.0
commands = black --check --diff .