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 .