From d626c11e30fa3eff54ef4a89ac3aa0a0a6beffb9 Mon Sep 17 00:00:00 2001 From: Christian Bean Date: Mon, 23 May 2022 14:35:35 +0200 Subject: [PATCH 01/19] some incomplete code for general fusion --- tilings/algorithms/fusion.py | 552 +++++++++++++++++++++-------------- 1 file changed, 335 insertions(+), 217 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 33aba358..02915f2f 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -5,6 +5,7 @@ from itertools import chain from typing import TYPE_CHECKING, Counter, Iterable, Iterator, List, Optional, Tuple +from tilings.algorithms.minimal_gridded_perms import MinimalGriddedPerms from tilings.assumptions import ( ComponentAssumption, SkewComponentAssumption, @@ -407,20 +408,6 @@ def fused_tiling(self) -> "Tiling": class ComponentFusion(Fusion): - """ - Component Fusion algorithm container class. - - Fuse tiling it it can be unfused by drawing a line between any component. - - Check if a fusion is valid and compute the fused tiling. - - If `row_idx` is provided it attempts to fuse row `row_idx` with row - `row_idx+1`. - - If `col_idx` is provided it attempts to fuse column `col_idx` with - column `col_idx+1`. - """ - def __init__( self, tiling: "Tiling", @@ -430,10 +417,9 @@ def __init__( tracked: bool = False, isolation_level: Optional[str] = None, ): - if tiling.requirements: - raise NotImplementedError( - "Component fusion does not handle " "requirements at the moment" - ) + self._unfused_obs_and_reqs: Optional[ + Tuple[Tuple[GriddedPerm, ...], Tuple[Tuple[GriddedPerm, ...], ...]] + ] = None super().__init__( tiling, row_idx=row_idx, @@ -441,217 +427,349 @@ def __init__( tracked=tracked, isolation_level=isolation_level, ) - self._first_cell: Optional[Cell] = None - self._second_cell: Optional[Cell] = None - def _pre_check(self) -> bool: - """ - Make a preliminary check before testing if the actual fusion is - possible. - - Selects the two active cells to be fused. Rows or columns with more - than one active cell cannot be fused. Sets the attribute - `self._first_cell` and `self._second_cell`. - """ - if self._fuse_row: - rows = ( - self._tiling.cells_in_row(self._row_idx), - self._tiling.cells_in_row(self._row_idx + 1), - ) - else: - rows = ( - self._tiling.cells_in_col(self._col_idx), - self._tiling.cells_in_col(self._col_idx + 1), - ) - has_a_long_row = any(len(row) > 1 for row in rows) - if has_a_long_row: - return False - first_cell = next(iter(rows[0])) - second_cell = next(iter(rows[1])) - cells_are_adjacent = ( - first_cell[0] == second_cell[0] or first_cell[1] == second_cell[1] - ) - if not cells_are_adjacent: - return False - same_basis = ( - self._tiling.cell_basis()[first_cell][0] - == self._tiling.cell_basis()[second_cell][0] + @staticmethod + def min_gps(requirements: Iterable[Iterable[GriddedPerm]]): + requirements = tuple( + sorted(set(tuple(sorted(set(gps))) for gps in requirements)) ) - if not same_basis: - return False - self._first_cell = first_cell - self._second_cell = second_cell - return True - - @property - def first_cell(self) -> Cell: - """ - The first cell of the fusion. This cell is in the bottommost row or the - leftmost column of the fusion. - """ - if self._first_cell is not None: - return self._first_cell - if not self._pre_check(): - raise RuntimeError( - "Pre-check failed. No component fusion " "possible and no first cell" - ) - assert self._first_cell is not None - return self._first_cell - - @property - def second_cell(self) -> Cell: - """ - The second cell of the fusion. This cell is in the topmost row or the - rightmost column of the fusion. - """ - if self._second_cell is not None: - return self._second_cell - if not self._pre_check(): - raise RuntimeError( - "Pre-check failed. No component fusion " "possible and no second cell" - ) - assert self._second_cell is not None - return self._second_cell + algo = MinimalGriddedPerms(tuple(), requirements) + return tuple(algo.minimal_gridded_perms()) - def has_crossing_len2_ob(self) -> bool: - """ - Return True if the tiling contains a crossing length 2 obstruction - between `self.first_cell` and `self.second_cell`. - """ - fcell = self.first_cell - scell = self.second_cell - if self._fuse_row: - possible_obs = [ - GriddedPerm((0, 1), (fcell, scell)), - GriddedPerm((1, 0), (scell, fcell)), - ] - else: - possible_obs = [ - GriddedPerm((0, 1), (fcell, scell)), - GriddedPerm((1, 0), (fcell, scell)), - ] - return any(ob in possible_obs for ob in self._tiling.obstructions) + def fuse_gridded_perms(self, gps: Iterable[GriddedPerm]) -> Tuple[GriddedPerm, ...]: + return tuple(sorted(set(self.fuse_gridded_perm(gp) for gp in gps))) - def is_crossing_len2(self, gp: GriddedPerm) -> bool: - """ - Return True if the gridded permutation `gp` is a length 2 obstruction - crossing between the first and second cell. - """ - return ( - len(gp) == 2 - and gp.occupies(self.first_cell) - and gp.occupies(self.second_cell) - ) + def is_left_gp(self, gp: GriddedPerm): + if self._col_idx is not None: + return all(x <= self._col_idx for x, _ in gp.pos) + return all(y <= self._col_idx for _, y in gp.pos) - @property - def obstruction_fuse_counter(self) -> Counter[GriddedPerm]: - """ - Counter of multiplicities of fused obstructions. + def is_right_gp(self, gp: GriddedPerm): + if self._col_idx is not None: + return all(x > self._col_idx for x, _ in gp.pos) + return all(y > self._col_idx for _, y in gp.pos) - Crossing length 2 obstructions between first cell and second cell - are ignored. - """ - if self._obstruction_fuse_counter is not None: - return self._obstruction_fuse_counter - obs = (ob for ob in self._tiling.obstructions if not self.is_crossing_len2(ob)) - fuse_counter = self._fuse_counter(obs) - self._obstruction_fuse_counter = fuse_counter - return self._obstruction_fuse_counter - - def obstructions_to_add(self) -> Iterator[GriddedPerm]: - """ - Iterator over all the obstructions obtained by fusing obstructions of - the tiling and then unfusing it in all possible ways. Crossing length 2 - obstructions between first cell and second cell are not processed. - """ - return chain.from_iterable( - self.unfuse_gridded_perm(ob) for ob in self.obstruction_fuse_counter - ) - - def _can_component_fuse_assumption(self, assumption: TrackingAssumption) -> bool: - """ - Return True if an assumption can be fused. That is, prefusion, the gps - do not touch the fuse region unless it is the correct sum or skew - 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() + def fused_tiling(self) -> "Tiling": + if self._fused_tiling is None: + obs = self.min_gps( + ( + self.fuse_gridded_perms( + gp for gp in self._tiling.obstructions if self.is_left_gp(gp) + ), + self.fuse_gridded_perms( + gp for gp in self._tiling.obstructions if self.is_right_gp(gp) + ), + ) ) - 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( - self, fuse_counter: Counter[GriddedPerm] - ) -> bool: - raise NotImplementedError + reqs = tuple( + self.min_gps( + ( + self.fuse_gridded_perms( + gp for gp in req_list if self.is_left_gp(gp) + ), + self.fuse_gridded_perms( + gp for gp in req_list if self.is_right_gp(gp) + ), + ) + ) + for req_list in self._tiling.requirements + ) + ass = tuple( + ass.__class__((self.fuse_gridded_perm(gp) for gp in ass.gps)) + for ass in self._tiling.assumptions + ) + if self._tracked: + ass = ass + (self.new_assumption(),) + self._fused_tiling = self._tiling.__class__(obs, reqs, ass) + return self._fused_tiling - def _is_valid_count(self, count: int, gp: GriddedPerm) -> bool: - raise NotImplementedError + def unfused_fused_obs_reqs( + self, + ) -> Tuple[Tuple[GriddedPerm, ...], Tuple[Tuple[GriddedPerm, ...], ...]]: + if self._unfused_obs_and_reqs is None: + fused_tiling = self.fused_tiling() + obs = tuple( + chain.from_iterable( + self.unfuse_gridded_perm(gp) for gp in fused_tiling.obstructions + ) + ) + reqs = tuple( + tuple(chain.from_iterable(self.unfuse_gridded_perm(gp) for gp in req)) + for req in fused_tiling.requirements + ) + self._unfused_obs_and_reqs = obs, reqs + return self._unfused_obs_and_reqs def fusable(self) -> bool: - """ - Return True if adjacent rows can be viewed as one row where you draw a - horizontal line through the components. - """ - if not self._pre_check() or not self.has_crossing_len2_ob(): - return False - new_tiling = self._tiling.add_obstructions(self.obstructions_to_add()) - + obs, reqs = self.unfused_fused_obs_reqs() return ( - self._tiling == new_tiling - and self._check_isolation_level() - and all( - self._can_component_fuse_assumption(assumption) - for assumption in self._tiling.assumptions + self._tiling + == self._tiling.__class__( + self._tiling.obstructions + obs, + self._tiling.requirements + reqs, + self._tiling.assumptions, ) + and self.has_suitable_extra_obs_and_reqs() ) - def new_assumption(self) -> ComponentAssumption: - """ - Return the assumption that needs to be counted in order to enumerate. - """ - fcell = self.first_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)) - 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 extra_obs_and_reqs( + self, + ) -> Tuple[Tuple[GriddedPerm, ...], Tuple[Tuple[GriddedPerm, ...], ...]]: + obs, reqs = self.unfused_fused_obs_reqs() + return tuple(ob for ob in self._tiling.obstructions if ob not in obs), tuple( + req for req in self._tiling.requirements if req not in reqs + ) - def __str__(self) -> str: - s = "ComponentFusion Algorithm for:\n" - s += str(self._tiling) - return s + def has_suitable_extra_obs_and_reqs(self) -> bool: + obs, reqs = self.extra_obs_and_reqs() + + +# class ComponentFusion(Fusion): +# """ +# Component Fusion algorithm container class. + +# Fuse tiling it it can be unfused by drawing a line between any component. + +# Check if a fusion is valid and compute the fused tiling. + +# If `row_idx` is provided it attempts to fuse row `row_idx` with row +# `row_idx+1`. + +# If `col_idx` is provided it attempts to fuse column `col_idx` with +# column `col_idx+1`. +# """ + +# def __init__( +# self, +# tiling: "Tiling", +# *, +# row_idx: Optional[int] = None, +# col_idx: Optional[int] = None, +# tracked: bool = False, +# isolation_level: Optional[str] = None, +# ): +# if tiling.requirements: +# raise NotImplementedError( +# "Component fusion does not handle " "requirements at the moment" +# ) +# super().__init__( +# tiling, +# row_idx=row_idx, +# col_idx=col_idx, +# tracked=tracked, +# isolation_level=isolation_level, +# ) +# self._first_cell: Optional[Cell] = None +# self._second_cell: Optional[Cell] = None + +# def _pre_check(self) -> bool: +# """ +# Make a preliminary check before testing if the actual fusion is +# possible. + +# Selects the two active cells to be fused. Rows or columns with more +# than one active cell cannot be fused. Sets the attribute +# `self._first_cell` and `self._second_cell`. +# """ +# if self._fuse_row: +# rows = ( +# self._tiling.cells_in_row(self._row_idx), +# self._tiling.cells_in_row(self._row_idx + 1), +# ) +# else: +# rows = ( +# self._tiling.cells_in_col(self._col_idx), +# self._tiling.cells_in_col(self._col_idx + 1), +# ) +# has_a_long_row = any(len(row) > 1 for row in rows) +# if has_a_long_row: +# return False +# first_cell = next(iter(rows[0])) +# second_cell = next(iter(rows[1])) +# cells_are_adjacent = ( +# first_cell[0] == second_cell[0] or first_cell[1] == second_cell[1] +# ) +# if not cells_are_adjacent: +# return False +# same_basis = ( +# self._tiling.cell_basis()[first_cell][0] +# == self._tiling.cell_basis()[second_cell][0] +# ) +# if not same_basis: +# return False +# self._first_cell = first_cell +# self._second_cell = second_cell +# return True + +# @property +# def first_cell(self) -> Cell: +# """ +# The first cell of the fusion. This cell is in the bottommost row or the +# leftmost column of the fusion. +# """ +# if self._first_cell is not None: +# return self._first_cell +# if not self._pre_check(): +# raise RuntimeError( +# "Pre-check failed. No component fusion " "possible and no first cell" +# ) +# assert self._first_cell is not None +# return self._first_cell + +# @property +# def second_cell(self) -> Cell: +# """ +# The second cell of the fusion. This cell is in the topmost row or the +# rightmost column of the fusion. +# """ +# if self._second_cell is not None: +# return self._second_cell +# if not self._pre_check(): +# raise RuntimeError( +# "Pre-check failed. No component fusion " "possible and no second cell" +# ) +# assert self._second_cell is not None +# return self._second_cell + +# def has_crossing_len2_ob(self) -> bool: +# """ +# Return True if the tiling contains a crossing length 2 obstruction +# between `self.first_cell` and `self.second_cell`. +# """ +# fcell = self.first_cell +# scell = self.second_cell +# if self._fuse_row: +# possible_obs = [ +# GriddedPerm((0, 1), (fcell, scell)), +# GriddedPerm((1, 0), (scell, fcell)), +# ] +# else: +# possible_obs = [ +# GriddedPerm((0, 1), (fcell, scell)), +# GriddedPerm((1, 0), (fcell, scell)), +# ] +# return any(ob in possible_obs for ob in self._tiling.obstructions) + +# def is_crossing_len2(self, gp: GriddedPerm) -> bool: +# """ +# Return True if the gridded permutation `gp` is a length 2 obstruction +# crossing between the first and second cell. +# """ +# return ( +# len(gp) == 2 +# and gp.occupies(self.first_cell) +# and gp.occupies(self.second_cell) +# ) + +# @property +# def obstruction_fuse_counter(self) -> Counter[GriddedPerm]: +# """ +# Counter of multiplicities of fused obstructions. + +# Crossing length 2 obstructions between first cell and second cell +# are ignored. +# """ +# if self._obstruction_fuse_counter is not None: +# return self._obstruction_fuse_counter +# obs = (ob for ob in self._tiling.obstructions if not self.is_crossing_len2(ob)) +# fuse_counter = self._fuse_counter(obs) +# self._obstruction_fuse_counter = fuse_counter +# return self._obstruction_fuse_counter + +# def obstructions_to_add(self) -> Iterator[GriddedPerm]: +# """ +# Iterator over all the obstructions obtained by fusing obstructions of +# the tiling and then unfusing it in all possible ways. Crossing length 2 +# obstructions between first cell and second cell are not processed. +# """ +# return chain.from_iterable( +# self.unfuse_gridded_perm(ob) for ob in self.obstruction_fuse_counter +# ) + +# def _can_component_fuse_assumption(self, assumption: TrackingAssumption) -> bool: +# """ +# Return True if an assumption can be fused. That is, prefusion, the gps +# do not touch the fuse region unless it is the correct sum or skew +# 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( +# self, fuse_counter: Counter[GriddedPerm] +# ) -> bool: +# raise NotImplementedError + +# def _is_valid_count(self, count: int, gp: GriddedPerm) -> bool: +# raise NotImplementedError + +# def fusable(self) -> bool: +# """ +# Return True if adjacent rows can be viewed as one row where you draw a +# horizontal line through the components. +# """ +# if not self._pre_check() or not self.has_crossing_len2_ob(): +# return False +# new_tiling = self._tiling.add_obstructions(self.obstructions_to_add()) + +# 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 +# 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)) +# 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" +# s += str(self._tiling) +# return s From 2cdd40a2ddb6efbff0cb5338aa84355664cda955 Mon Sep 17 00:00:00 2001 From: Christian Bean Date: Mon, 30 May 2022 11:53:11 +0000 Subject: [PATCH 02/19] compute gap vectors --- tilings/algorithms/fusion.py | 76 +++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 02915f2f..283004eb 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, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, +) from tilings.algorithms.minimal_gridded_perms import MinimalGriddedPerms from tilings.assumptions import ( @@ -439,25 +448,34 @@ def min_gps(requirements: Iterable[Iterable[GriddedPerm]]): def fuse_gridded_perms(self, gps: Iterable[GriddedPerm]) -> Tuple[GriddedPerm, ...]: return tuple(sorted(set(self.fuse_gridded_perm(gp) for gp in gps))) - def is_left_gp(self, gp: GriddedPerm): + def is_left_gp(self, gp: GriddedPerm) -> bool: + if self._col_idx is not None: + return any(x == self._col_idx for x, _ in gp.pos) + return any(y == self._row_idx for _, y in gp.pos) + + def is_right_gp(self, gp: GriddedPerm) -> bool: if self._col_idx is not None: - return all(x <= self._col_idx for x, _ in gp.pos) - return all(y <= self._col_idx for _, y in gp.pos) + return any(x == 1 + self._col_idx for x, _ in gp.pos) + return any(y == 1 + self._row_idx for _, y in gp.pos) - def is_right_gp(self, gp: GriddedPerm): + def is_in_fuse_region(self, gp: GriddedPerm) -> bool: if self._col_idx is not None: - return all(x > self._col_idx for x, _ in gp.pos) - return all(y > self._col_idx for _, y in gp.pos) + return all(x in (self._col_idx, self._col_idx + 1) for x, _ in gp.pos) + return all(y in (self._row_idx, self._row_idx + 1) for _, y in gp.pos) def fused_tiling(self) -> "Tiling": if self._fused_tiling is None: obs = self.min_gps( ( self.fuse_gridded_perms( - gp for gp in self._tiling.obstructions if self.is_left_gp(gp) + gp + for gp in self._tiling.obstructions + if not self.is_left_gp(gp) ), self.fuse_gridded_perms( - gp for gp in self._tiling.obstructions if self.is_right_gp(gp) + gp + for gp in self._tiling.obstructions + if not self.is_right_gp(gp) ), ) ) @@ -465,10 +483,10 @@ def fused_tiling(self) -> "Tiling": self.min_gps( ( self.fuse_gridded_perms( - gp for gp in req_list if self.is_left_gp(gp) + gp for gp in req_list if not self.is_left_gp(gp) ), self.fuse_gridded_perms( - gp for gp in req_list if self.is_right_gp(gp) + gp for gp in req_list if not self.is_right_gp(gp) ), ) ) @@ -509,7 +527,7 @@ def fusable(self) -> bool: self._tiling.requirements + reqs, self._tiling.assumptions, ) - and self.has_suitable_extra_obs_and_reqs() + and self.is_finite_fusable() ) def extra_obs_and_reqs( @@ -520,8 +538,38 @@ def extra_obs_and_reqs( req for req in self._tiling.requirements if req not in reqs ) - def has_suitable_extra_obs_and_reqs(self) -> bool: - obs, reqs = self.extra_obs_and_reqs() + def get_vector_from_gp(self, gp: GriddedPerm) -> Tuple[int, int]: + if self._col_idx is not None: + return ( + sum(1 for x, _ in gp.pos if x == self._col_idx), + sum(1 for x, _ in gp.pos if x == self._col_idx + 1), + ) + return ( + sum(1 for _, y in gp.pos if y == self._row_idx), + sum(1 for _, y in gp.pos if y == self._row_idx + 1), + ) + + def get_sk_from_gap_vector(self, vector: Tuple[int, int]): + pass + + def is_finite_fusable(self) -> bool: + obs, _ = self.extra_obs_and_reqs() + gap_vectors: Set[Tuple[int, int]] = set() + for ob in obs: + if not self.is_in_fuse_region(ob): + return False + gap_vectors.add(self.get_vector_from_gp(ob)) + + # for vector in gap_vectors: + # sk = a + # if self._tiling.add_obstructions(sk) != self._tiling: + # return False + + return all( + self._tiling.add_obstructions(self.get_sk_from_gap_vector(vector)) + == self._tiling + for vector in gap_vectors + ) # class ComponentFusion(Fusion): From 3bf66d30a0664cfa7601d2c6e80e0874ae07cf39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Mon, 30 May 2022 14:21:04 +0000 Subject: [PATCH 03/19] pre-image function for row col map --- tilings/algorithms/map.py | 78 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/tilings/algorithms/map.py b/tilings/algorithms/map.py index feaf2d21..976068ed 100644 --- a/tilings/algorithms/map.py +++ b/tilings/algorithms/map.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING, Dict, Tuple +import itertools +from typing import TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, Tuple from tilings.exception import InvalidOperationError @@ -117,6 +118,81 @@ def map_col(self, col: int) -> int: """ return self._col_map[col] + # Pre-image method + def preimage_row(self, row: int) -> Iterator[int]: + """Returns all the preimages of the given row.""" + return (k for k, v in self._row_map.items() if v == row) + + def preimage_col(self, col: int) -> Iterator[int]: + """Returns all the preimages of the given column.""" + return (k for k, v in self._col_map.items() if v == col) + + def preimage_cell(self, cell: Cell) -> Iterator[Cell]: + """Returns all the preimages of the given cell.""" + col, row = cell + return itertools.product(self.preimage_col(col), self.preimage_row(row)) + + @staticmethod + def _preimage_gp_col( + gp_cols: Tuple[int, ...], preimage_func: Callable[[int], Iterator[int]] + ) -> Iterator[Tuple[int, ...]]: + """ + Return all the possible sequence of column for a preimage of the gridded + permutation using the given preimage_func. + """ + possible_col = [sorted(preimage_func(col)) for col in gp_cols] + partial_pos: List[int] = [] + partial_pos_indices: List[int] = [] + while True: + # Padding the current solution with the leftmost options + while len(partial_pos) < len(gp_cols): + last_col = partial_pos[-1] if partial_pos else 0 + for new_col_idx, col in enumerate(possible_col[len(partial_pos)]): + if last_col <= col: + partial_pos.append(col) + partial_pos_indices.append(new_col_idx) + break + else: + break + else: + yield tuple(partial_pos) + # increasing the rightmost pos that can be increased. + while partial_pos: + partial_pos.pop() + partial_pos_last_index = partial_pos_indices.pop() + if partial_pos_last_index + 1 < len(possible_col[len(partial_pos)]): + break + else: + break + partial_pos.append( + possible_col[len(partial_pos)][partial_pos_last_index + 1] + ) + partial_pos_indices.append(partial_pos_last_index + 1) + + def preimage_gp(self, gp: "GriddedPerm") -> Iterator["GriddedPerm"]: + """ + Returns all the preimages of the given gridded permutation. + Gridded permutations that are contradictory are filtered out. + """ + gp_cols = tuple(col for col, _ in gp.pos) + preimage_col_pos = self._preimage_gp_col(gp_cols, self.preimage_col) + gp_rows = gp.patt.inverse().apply(row for _, row in gp.pos) + preimage_row_pos: Iterator[Tuple[int, ...]] = map( + gp.patt.apply, self._preimage_gp_col(gp_rows, self.preimage_row) + ) + for pos in itertools.product(preimage_col_pos, preimage_row_pos): + new_gp = gp.__class__(gp.patt, zip(*pos)) + yield new_gp + + def preimage_gps(self, gps: Iterable["GriddedPerm"]) -> Iterator["GriddedPerm"]: + """ + Returns all the preimages of the given gridded permutations. + Gridded permutations that are contradictory are filtered out. + """ + for gp in gps: + yield from self.preimage_gp(gp) + + # Other method def max_row(self) -> int: """Return the biggest row index in the image.""" return max(self._row_map.values()) From 626577e5354bf63774fabb8bfc03648bb1f8f9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Mon, 30 May 2022 14:22:33 +0000 Subject: [PATCH 04/19] computing the crossing for a given gap vector --- tilings/algorithms/fusion.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 283004eb..2197591e 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -14,6 +14,8 @@ Tuple, ) +from permuta import Perm +from tilings.algorithms.map import RowColMap from tilings.algorithms.minimal_gridded_perms import MinimalGriddedPerms from tilings.assumptions import ( ComponentAssumption, @@ -549,8 +551,36 @@ def get_vector_from_gp(self, gp: GriddedPerm) -> Tuple[int, int]: sum(1 for _, y in gp.pos if y == self._row_idx + 1), ) - def get_sk_from_gap_vector(self, vector: Tuple[int, int]): - pass + def get_sk_from_gap_vector(self, vector: Tuple[int, int]) -> Iterator[GriddedPerm]: + if not self._fuse_row: + row_map = RowColMap( + row_map={x: 0 for x in range(self._tiling.dimensions[1])}, + col_map={0: 0}, + is_identity=False, + ) + col_pos = (self._col_idx,) * vector[0] + (self._col_idx + 1,) * vector[1] + Sk = ( + GriddedPerm.single_cell(p, (0, 0)) for p in Perm.of_length(sum(vector)) + ) + for gp in row_map.preimage_gps(Sk): + new_pos = tuple((x, y) for (_, y), x in zip(gp.pos, col_pos)) + yield GriddedPerm(gp.patt, new_pos) + else: + col_map = RowColMap( + row_map={0: 0}, + col_map={x: 0 for x in range(self._tiling.dimensions[0])}, + is_identity=False, + ) + Sk = ( + GriddedPerm.single_cell(p, (0, 0)) for p in Perm.of_length(sum(vector)) + ) + row_heights = (self._row_idx,) * vector[0] + (self._row_idx + 1,) * vector[ + 1 + ] + for gp in col_map.preimage_gps(Sk): + row_pos = gp.patt.apply(row_heights) + new_pos = tuple((x, y) for (x, _), y in zip(gp.pos, row_pos)) + yield GriddedPerm(gp.patt, new_pos) def is_finite_fusable(self) -> bool: obs, _ = self.extra_obs_and_reqs() From 6f192a0b48adb444c0605a290691aed3dc8e8e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Mon, 30 May 2022 15:41:29 +0000 Subject: [PATCH 05/19] finite fusion algorithm seems to work --- tilings/algorithms/fusion.py | 613 ++++++++++++++++------------------- 1 file changed, 286 insertions(+), 327 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 2197591e..34faef44 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -419,6 +419,20 @@ def fused_tiling(self) -> "Tiling": class ComponentFusion(Fusion): + """ + Component Fusion algorithm container class. + + Fuse tiling it it can be unfused by drawing a line between any component. + + Check if a fusion is valid and compute the fused tiling. + + If `row_idx` is provided it attempts to fuse row `row_idx` with row + `row_idx+1`. + + If `col_idx` is provided it attempts to fuse column `col_idx` with + column `col_idx+1`. + """ + def __init__( self, tiling: "Tiling", @@ -428,9 +442,10 @@ def __init__( tracked: bool = False, isolation_level: Optional[str] = None, ): - self._unfused_obs_and_reqs: Optional[ - Tuple[Tuple[GriddedPerm, ...], Tuple[Tuple[GriddedPerm, ...], ...]] - ] = None + if tiling.requirements: + raise NotImplementedError( + "Component fusion does not handle " "requirements at the moment" + ) super().__init__( tiling, row_idx=row_idx, @@ -438,27 +453,241 @@ def __init__( tracked=tracked, isolation_level=isolation_level, ) + self._first_cell: Optional[Cell] = None + self._second_cell: Optional[Cell] = None + + def _pre_check(self) -> bool: + """ + Make a preliminary check before testing if the actual fusion is + possible. - @staticmethod - def min_gps(requirements: Iterable[Iterable[GriddedPerm]]): - requirements = tuple( - sorted(set(tuple(sorted(set(gps))) for gps in requirements)) + Selects the two active cells to be fused. Rows or columns with more + than one active cell cannot be fused. Sets the attribute + `self._first_cell` and `self._second_cell`. + """ + if self._fuse_row: + rows = ( + self._tiling.cells_in_row(self._row_idx), + self._tiling.cells_in_row(self._row_idx + 1), + ) + else: + rows = ( + self._tiling.cells_in_col(self._col_idx), + self._tiling.cells_in_col(self._col_idx + 1), + ) + has_a_long_row = any(len(row) > 1 for row in rows) + if has_a_long_row: + return False + first_cell = next(iter(rows[0])) + second_cell = next(iter(rows[1])) + cells_are_adjacent = ( + first_cell[0] == second_cell[0] or first_cell[1] == second_cell[1] ) - algo = MinimalGriddedPerms(tuple(), requirements) - return tuple(algo.minimal_gridded_perms()) + if not cells_are_adjacent: + return False + same_basis = ( + self._tiling.cell_basis()[first_cell][0] + == self._tiling.cell_basis()[second_cell][0] + ) + if not same_basis: + return False + self._first_cell = first_cell + self._second_cell = second_cell + return True - def fuse_gridded_perms(self, gps: Iterable[GriddedPerm]) -> Tuple[GriddedPerm, ...]: - return tuple(sorted(set(self.fuse_gridded_perm(gp) for gp in gps))) + @property + def first_cell(self) -> Cell: + """ + The first cell of the fusion. This cell is in the bottommost row or the + leftmost column of the fusion. + """ + if self._first_cell is not None: + return self._first_cell + if not self._pre_check(): + raise RuntimeError( + "Pre-check failed. No component fusion " "possible and no first cell" + ) + assert self._first_cell is not None + return self._first_cell - def is_left_gp(self, gp: GriddedPerm) -> bool: - if self._col_idx is not None: - return any(x == self._col_idx for x, _ in gp.pos) - return any(y == self._row_idx for _, y in gp.pos) + @property + def second_cell(self) -> Cell: + """ + The second cell of the fusion. This cell is in the topmost row or the + rightmost column of the fusion. + """ + if self._second_cell is not None: + return self._second_cell + if not self._pre_check(): + raise RuntimeError( + "Pre-check failed. No component fusion " "possible and no second cell" + ) + assert self._second_cell is not None + return self._second_cell - def is_right_gp(self, gp: GriddedPerm) -> bool: - if self._col_idx is not None: - return any(x == 1 + self._col_idx for x, _ in gp.pos) - return any(y == 1 + self._row_idx for _, y in gp.pos) + def has_crossing_len2_ob(self) -> bool: + """ + Return True if the tiling contains a crossing length 2 obstruction + between `self.first_cell` and `self.second_cell`. + """ + fcell = self.first_cell + scell = self.second_cell + if self._fuse_row: + possible_obs = [ + GriddedPerm((0, 1), (fcell, scell)), + GriddedPerm((1, 0), (scell, fcell)), + ] + else: + possible_obs = [ + GriddedPerm((0, 1), (fcell, scell)), + GriddedPerm((1, 0), (fcell, scell)), + ] + return any(ob in possible_obs for ob in self._tiling.obstructions) + + def is_crossing_len2(self, gp: GriddedPerm) -> bool: + """ + Return True if the gridded permutation `gp` is a length 2 obstruction + crossing between the first and second cell. + """ + return ( + len(gp) == 2 + and gp.occupies(self.first_cell) + and gp.occupies(self.second_cell) + ) + + @property + def obstruction_fuse_counter(self) -> Counter[GriddedPerm]: + """ + Counter of multiplicities of fused obstructions. + + Crossing length 2 obstructions between first cell and second cell + are ignored. + """ + if self._obstruction_fuse_counter is not None: + return self._obstruction_fuse_counter + obs = (ob for ob in self._tiling.obstructions if not self.is_crossing_len2(ob)) + fuse_counter = self._fuse_counter(obs) + self._obstruction_fuse_counter = fuse_counter + return self._obstruction_fuse_counter + + def obstructions_to_add(self) -> Iterator[GriddedPerm]: + """ + Iterator over all the obstructions obtained by fusing obstructions of + the tiling and then unfusing it in all possible ways. Crossing length 2 + obstructions between first cell and second cell are not processed. + """ + return chain.from_iterable( + self.unfuse_gridded_perm(ob) for ob in self.obstruction_fuse_counter + ) + + def _can_component_fuse_assumption(self, assumption: TrackingAssumption) -> bool: + """ + Return True if an assumption can be fused. That is, prefusion, the gps + do not touch the fuse region unless it is the correct sum or skew + 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( + self, fuse_counter: Counter[GriddedPerm] + ) -> bool: + raise NotImplementedError + + def _is_valid_count(self, count: int, gp: GriddedPerm) -> bool: + raise NotImplementedError + + def fusable(self) -> bool: + """ + Return True if adjacent rows can be viewed as one row where you draw a + horizontal line through the components. + """ + if not self._pre_check() or not self.has_crossing_len2_ob(): + return False + new_tiling = self._tiling.add_obstructions(self.obstructions_to_add()) + + 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 + 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)) + 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" + s += str(self._tiling) + + +class FiniteFusion(Fusion): + def __init__( + self, + tiling: "Tiling", + *, + row_idx: Optional[int] = None, + col_idx: Optional[int] = None, + tracked: bool = False, + isolation_level: Optional[str] = None, + ): + self._unfused_obs_and_reqs: Optional[ + Tuple[Tuple[GriddedPerm, ...], Tuple[Tuple[GriddedPerm, ...], ...]] + ] = None + super().__init__( + tiling, + row_idx=row_idx, + col_idx=col_idx, + tracked=tracked, + isolation_level=isolation_level, + ) def is_in_fuse_region(self, gp: GriddedPerm) -> bool: if self._col_idx is not None: @@ -467,40 +696,23 @@ def is_in_fuse_region(self, gp: GriddedPerm) -> bool: def fused_tiling(self) -> "Tiling": if self._fused_tiling is None: - obs = self.min_gps( - ( - self.fuse_gridded_perms( - gp - for gp in self._tiling.obstructions - if not self.is_left_gp(gp) - ), - self.fuse_gridded_perms( - gp - for gp in self._tiling.obstructions - if not self.is_right_gp(gp) - ), - ) + extra_obs = self.extra_obs() + fused_obs = set( + self.fuse_gridded_perm(ob) + for ob in self._tiling.obstructions + if not ob in extra_obs ) - reqs = tuple( - self.min_gps( - ( - self.fuse_gridded_perms( - gp for gp in req_list if not self.is_left_gp(gp) - ), - self.fuse_gridded_perms( - gp for gp in req_list if not self.is_right_gp(gp) - ), - ) - ) - for req_list in self._tiling.requirements + fused_reqs = set( + frozenset(self.fuse_gridded_perm(gp) for gp in req) + for req in self._tiling.requirements ) - ass = tuple( - ass.__class__((self.fuse_gridded_perm(gp) for gp in ass.gps)) + fused_ass = set( + ass.__class__([self.fuse_gridded_perm(gp) for gp in ass.gps]) for ass in self._tiling.assumptions ) - if self._tracked: - ass = ass + (self.new_assumption(),) - self._fused_tiling = self._tiling.__class__(obs, reqs, ass) + self._fused_tiling = self._tiling.__class__( + fused_obs, fused_reqs, fused_ass + ) return self._fused_tiling def unfused_fused_obs_reqs( @@ -522,24 +734,26 @@ def unfused_fused_obs_reqs( def fusable(self) -> bool: obs, reqs = self.unfused_fused_obs_reqs() - return ( - self._tiling - == self._tiling.__class__( - self._tiling.obstructions + obs, - self._tiling.requirements + reqs, - self._tiling.assumptions, - ) - and self.is_finite_fusable() + return self._tiling == self._tiling.__class__( + self._tiling.obstructions + obs, + self._tiling.requirements + reqs, + self._tiling.assumptions, ) - def extra_obs_and_reqs( + def extra_obs( self, - ) -> Tuple[Tuple[GriddedPerm, ...], Tuple[Tuple[GriddedPerm, ...], ...]]: - obs, reqs = self.unfused_fused_obs_reqs() - return tuple(ob for ob in self._tiling.obstructions if ob not in obs), tuple( - req for req in self._tiling.requirements if req not in reqs + ) -> Set[GriddedPerm]: + valid_gap_vectors = set(self.get_valid_gap_vectors()) + print(valid_gap_vectors) + return set( + ob + for ob in self._tiling.obstructions + if any(self.gp_satifies_gap_vector(ob, vec) for vec in valid_gap_vectors) ) + def gp_satifies_gap_vector(self, gp: GriddedPerm, vector: Tuple[int, int]) -> bool: + return self.is_in_fuse_region(gp) and self.get_vector_from_gp(gp) == vector + def get_vector_from_gp(self, gp: GriddedPerm) -> Tuple[int, int]: if self._col_idx is not None: return ( @@ -582,272 +796,17 @@ def get_sk_from_gap_vector(self, vector: Tuple[int, int]) -> Iterator[GriddedPer new_pos = tuple((x, y) for (x, _), y in zip(gp.pos, row_pos)) yield GriddedPerm(gp.patt, new_pos) - def is_finite_fusable(self) -> bool: - obs, _ = self.extra_obs_and_reqs() - gap_vectors: Set[Tuple[int, int]] = set() - for ob in obs: - if not self.is_in_fuse_region(ob): - return False - gap_vectors.add(self.get_vector_from_gp(ob)) - - # for vector in gap_vectors: - # sk = a - # if self._tiling.add_obstructions(sk) != self._tiling: - # return False - - return all( + def is_valid_gap_vector(self, vector: Tuple[int, int]) -> bool: + return ( self._tiling.add_obstructions(self.get_sk_from_gap_vector(vector)) == self._tiling - for vector in gap_vectors ) - -# class ComponentFusion(Fusion): -# """ -# Component Fusion algorithm container class. - -# Fuse tiling it it can be unfused by drawing a line between any component. - -# Check if a fusion is valid and compute the fused tiling. - -# If `row_idx` is provided it attempts to fuse row `row_idx` with row -# `row_idx+1`. - -# If `col_idx` is provided it attempts to fuse column `col_idx` with -# column `col_idx+1`. -# """ - -# def __init__( -# self, -# tiling: "Tiling", -# *, -# row_idx: Optional[int] = None, -# col_idx: Optional[int] = None, -# tracked: bool = False, -# isolation_level: Optional[str] = None, -# ): -# if tiling.requirements: -# raise NotImplementedError( -# "Component fusion does not handle " "requirements at the moment" -# ) -# super().__init__( -# tiling, -# row_idx=row_idx, -# col_idx=col_idx, -# tracked=tracked, -# isolation_level=isolation_level, -# ) -# self._first_cell: Optional[Cell] = None -# self._second_cell: Optional[Cell] = None - -# def _pre_check(self) -> bool: -# """ -# Make a preliminary check before testing if the actual fusion is -# possible. - -# Selects the two active cells to be fused. Rows or columns with more -# than one active cell cannot be fused. Sets the attribute -# `self._first_cell` and `self._second_cell`. -# """ -# if self._fuse_row: -# rows = ( -# self._tiling.cells_in_row(self._row_idx), -# self._tiling.cells_in_row(self._row_idx + 1), -# ) -# else: -# rows = ( -# self._tiling.cells_in_col(self._col_idx), -# self._tiling.cells_in_col(self._col_idx + 1), -# ) -# has_a_long_row = any(len(row) > 1 for row in rows) -# if has_a_long_row: -# return False -# first_cell = next(iter(rows[0])) -# second_cell = next(iter(rows[1])) -# cells_are_adjacent = ( -# first_cell[0] == second_cell[0] or first_cell[1] == second_cell[1] -# ) -# if not cells_are_adjacent: -# return False -# same_basis = ( -# self._tiling.cell_basis()[first_cell][0] -# == self._tiling.cell_basis()[second_cell][0] -# ) -# if not same_basis: -# return False -# self._first_cell = first_cell -# self._second_cell = second_cell -# return True - -# @property -# def first_cell(self) -> Cell: -# """ -# The first cell of the fusion. This cell is in the bottommost row or the -# leftmost column of the fusion. -# """ -# if self._first_cell is not None: -# return self._first_cell -# if not self._pre_check(): -# raise RuntimeError( -# "Pre-check failed. No component fusion " "possible and no first cell" -# ) -# assert self._first_cell is not None -# return self._first_cell - -# @property -# def second_cell(self) -> Cell: -# """ -# The second cell of the fusion. This cell is in the topmost row or the -# rightmost column of the fusion. -# """ -# if self._second_cell is not None: -# return self._second_cell -# if not self._pre_check(): -# raise RuntimeError( -# "Pre-check failed. No component fusion " "possible and no second cell" -# ) -# assert self._second_cell is not None -# return self._second_cell - -# def has_crossing_len2_ob(self) -> bool: -# """ -# Return True if the tiling contains a crossing length 2 obstruction -# between `self.first_cell` and `self.second_cell`. -# """ -# fcell = self.first_cell -# scell = self.second_cell -# if self._fuse_row: -# possible_obs = [ -# GriddedPerm((0, 1), (fcell, scell)), -# GriddedPerm((1, 0), (scell, fcell)), -# ] -# else: -# possible_obs = [ -# GriddedPerm((0, 1), (fcell, scell)), -# GriddedPerm((1, 0), (fcell, scell)), -# ] -# return any(ob in possible_obs for ob in self._tiling.obstructions) - -# def is_crossing_len2(self, gp: GriddedPerm) -> bool: -# """ -# Return True if the gridded permutation `gp` is a length 2 obstruction -# crossing between the first and second cell. -# """ -# return ( -# len(gp) == 2 -# and gp.occupies(self.first_cell) -# and gp.occupies(self.second_cell) -# ) - -# @property -# def obstruction_fuse_counter(self) -> Counter[GriddedPerm]: -# """ -# Counter of multiplicities of fused obstructions. - -# Crossing length 2 obstructions between first cell and second cell -# are ignored. -# """ -# if self._obstruction_fuse_counter is not None: -# return self._obstruction_fuse_counter -# obs = (ob for ob in self._tiling.obstructions if not self.is_crossing_len2(ob)) -# fuse_counter = self._fuse_counter(obs) -# self._obstruction_fuse_counter = fuse_counter -# return self._obstruction_fuse_counter - -# def obstructions_to_add(self) -> Iterator[GriddedPerm]: -# """ -# Iterator over all the obstructions obtained by fusing obstructions of -# the tiling and then unfusing it in all possible ways. Crossing length 2 -# obstructions between first cell and second cell are not processed. -# """ -# return chain.from_iterable( -# self.unfuse_gridded_perm(ob) for ob in self.obstruction_fuse_counter -# ) - -# def _can_component_fuse_assumption(self, assumption: TrackingAssumption) -> bool: -# """ -# Return True if an assumption can be fused. That is, prefusion, the gps -# do not touch the fuse region unless it is the correct sum or skew -# 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( -# self, fuse_counter: Counter[GriddedPerm] -# ) -> bool: -# raise NotImplementedError - -# def _is_valid_count(self, count: int, gp: GriddedPerm) -> bool: -# raise NotImplementedError - -# def fusable(self) -> bool: -# """ -# Return True if adjacent rows can be viewed as one row where you draw a -# horizontal line through the components. -# """ -# if not self._pre_check() or not self.has_crossing_len2_ob(): -# return False -# new_tiling = self._tiling.add_obstructions(self.obstructions_to_add()) - -# 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 -# 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)) -# 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" -# s += str(self._tiling) -# return s + def get_valid_gap_vectors(self) -> Iterator[Tuple[int, int]]: + gap_vectors = { + self.get_vector_from_gp(ob) + for ob in self._tiling.obstructions + if self.is_in_fuse_region(ob) + } + return filter(self.is_valid_gap_vector, gap_vectors) + return s From e28f326082c6de65d97f530f07f00f8605a6830a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Mon, 30 May 2022 16:56:05 +0000 Subject: [PATCH 06/19] return tracked tiling --- tilings/algorithms/fusion.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 34faef44..982a8b43 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -710,6 +710,8 @@ def fused_tiling(self) -> "Tiling": ass.__class__([self.fuse_gridded_perm(gp) for gp in ass.gps]) for ass in self._tiling.assumptions ) + if self._tracked: + fused_ass.add(self.new_assumption()) self._fused_tiling = self._tiling.__class__( fused_obs, fused_reqs, fused_ass ) From 6799a96f89b9a8d77b6de34e8f276db65a811a45 Mon Sep 17 00:00:00 2001 From: Christian Bean Date: Tue, 31 May 2022 09:47:15 +0000 Subject: [PATCH 07/19] stop fusing to Av() --- tilings/algorithms/fusion.py | 40 +++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 982a8b43..0fcf76be 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -16,7 +16,6 @@ from permuta import Perm from tilings.algorithms.map import RowColMap -from tilings.algorithms.minimal_gridded_perms import MinimalGriddedPerms from tilings.assumptions import ( ComponentAssumption, SkewComponentAssumption, @@ -666,6 +665,7 @@ def is_skew_component_fusion(self) -> bool: def __str__(self) -> str: s = "ComponentFusion Algorithm for:\n" s += str(self._tiling) + return s class FiniteFusion(Fusion): @@ -700,21 +700,29 @@ def fused_tiling(self) -> "Tiling": fused_obs = set( self.fuse_gridded_perm(ob) for ob in self._tiling.obstructions - if not ob in extra_obs - ) - fused_reqs = set( - frozenset(self.fuse_gridded_perm(gp) for gp in req) - for req in self._tiling.requirements - ) - fused_ass = set( - ass.__class__([self.fuse_gridded_perm(gp) for gp in ass.gps]) - for ass in self._tiling.assumptions - ) - if self._tracked: - fused_ass.add(self.new_assumption()) - self._fused_tiling = self._tiling.__class__( - fused_obs, fused_reqs, fused_ass + if ob not in extra_obs ) + if any( + ob.is_single_cell() and any(ob.get_points_row(self._row_idx)) + for ob in fused_obs + ): + fused_reqs = set( + frozenset(self.fuse_gridded_perm(gp) for gp in req) + for req in self._tiling.requirements + ) + fused_ass = set( + ass.__class__([self.fuse_gridded_perm(gp) for gp in ass.gps]) + for ass in self._tiling.assumptions + ) + if self._tracked: + fused_ass.add(self.new_assumption()) + self._fused_tiling = self._tiling.__class__( + fused_obs, fused_reqs, fused_ass + ) + else: + # in this case we will be creating an Av() cell, so it implies + # ordinary fusion is happening to a finite region. + self._fused_tiling = super().fused_tiling() return self._fused_tiling def unfused_fused_obs_reqs( @@ -746,7 +754,6 @@ def extra_obs( self, ) -> Set[GriddedPerm]: valid_gap_vectors = set(self.get_valid_gap_vectors()) - print(valid_gap_vectors) return set( ob for ob in self._tiling.obstructions @@ -811,4 +818,3 @@ def get_valid_gap_vectors(self) -> Iterator[Tuple[int, int]]: if self.is_in_fuse_region(ob) } return filter(self.is_valid_gap_vector, gap_vectors) - return s From de531dbeaf63a6e069662aa9132af2cff8638b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Tue, 31 May 2022 15:53:13 +0000 Subject: [PATCH 08/19] finite fusion strategy --- tilings/strategies/fusion/finite.py | 112 ++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tilings/strategies/fusion/finite.py diff --git a/tilings/strategies/fusion/finite.py b/tilings/strategies/fusion/finite.py new file mode 100644 index 00000000..47f01504 --- /dev/null +++ b/tilings/strategies/fusion/finite.py @@ -0,0 +1,112 @@ +from typing import Iterator, Optional, Tuple + +from comb_spec_searcher import Constructor, StrategyFactory +from comb_spec_searcher.strategies import Rule +from tilings import GriddedPerm, Tiling +from tilings.algorithms.fusion import FiniteFusion + +from .constructor import FusionConstructor +from .fusion import FusionStrategy + + +class FiniteFusionStrategy(FusionStrategy): + def __init__(self, vectors, row_idx=None, col_idx=None, tracked: bool = False): + super().__init__(row_idx=row_idx, col_idx=col_idx, tracked=tracked) + self.vectors = vectors + + def fusion_algorithm(self, tiling: Tiling) -> FiniteFusion: + return FiniteFusion( + tiling, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked + ) + + def formal_step(self) -> str: + fusing = "rows" if self.row_idx is not None else "columns" + idx = self.row_idx if self.row_idx is not None else self.col_idx + return f"finite fuse {fusing} {idx} and {idx+1} with vectors {self.vectors}" + + def backward_map( + self, + comb_class: Tiling, + objs: Tuple[Optional[GriddedPerm], ...], + children: Optional[Tuple[Tiling, ...]] = None, + left_points: Optional[int] = None, + ) -> Iterator[GriddedPerm]: + """ + The backward direction of the underlying bijection used for object + generation and sampling. + """ + raise NotImplementedError + + def is_positive_or_empty_fusion(self, tiling: Tiling) -> bool: + raise NotImplementedError + + def constructor( + self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None + ) -> FusionConstructor: + raise NotImplementedError + + def reverse_constructor( # pylint: disable=no-self-use + self, + idx: int, + comb_class: Tiling, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Constructor: + raise NotImplementedError + + +class FiniteFusionFactory(StrategyFactory[Tiling]): + def __init__(self, tracked: bool = False, isolation_level: Optional[str] = None): + self.tracked = tracked + self.isolation_level = isolation_level + + def __call__(self, comb_class: Tiling) -> Iterator[Rule]: + if comb_class.requirements: + return + cols, rows = comb_class.dimensions + for row_idx in range(rows - 1): + algo = FiniteFusion( + comb_class, + row_idx=row_idx, + tracked=self.tracked, + isolation_level=self.isolation_level, + ) + if algo.fusable(): + fused_tiling = algo.fused_tiling() + yield FiniteFusionStrategy( + set(algo.get_valid_gap_vectors()), + row_idx=row_idx, + tracked=self.tracked, + )(comb_class, (fused_tiling,)) + for col_idx in range(cols - 1): + algo = FiniteFusion( + comb_class, + col_idx=col_idx, + tracked=self.tracked, + isolation_level=self.isolation_level, + ) + if algo.fusable(): + fused_tiling = algo.fused_tiling() + yield FiniteFusionStrategy( + set(algo.get_valid_gap_vectors()), + col_idx=col_idx, + tracked=self.tracked, + )(comb_class, (fused_tiling,)) + + def __str__(self) -> str: + return f"{'tracked ' if self.tracked else ''}finite fusion" + + def __repr__(self) -> str: + return ( + self.__class__.__name__ + + f"(tracked={self.tracked}, isolation_level={self.isolation_level})" + ) + + def to_jsonable(self) -> dict: + d: dict = super().to_jsonable() + d["tracked"] = self.tracked + d["isolation_level"] = self.isolation_level + return d + + @classmethod + def from_dict(cls, d: dict) -> "FiniteFusionStrategy": + return cls(**d) From c45f688769ab42940402b78e85e2626a20ec97c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 11:17:29 +0000 Subject: [PATCH 09/19] remove gap vector strategy instead of finite fusion --- tilings/strategies/fusion/finite.py | 112 ---------------------- tilings/strategies/gap_vectors.py | 141 ++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 112 deletions(-) delete mode 100644 tilings/strategies/fusion/finite.py create mode 100644 tilings/strategies/gap_vectors.py diff --git a/tilings/strategies/fusion/finite.py b/tilings/strategies/fusion/finite.py deleted file mode 100644 index 47f01504..00000000 --- a/tilings/strategies/fusion/finite.py +++ /dev/null @@ -1,112 +0,0 @@ -from typing import Iterator, Optional, Tuple - -from comb_spec_searcher import Constructor, StrategyFactory -from comb_spec_searcher.strategies import Rule -from tilings import GriddedPerm, Tiling -from tilings.algorithms.fusion import FiniteFusion - -from .constructor import FusionConstructor -from .fusion import FusionStrategy - - -class FiniteFusionStrategy(FusionStrategy): - def __init__(self, vectors, row_idx=None, col_idx=None, tracked: bool = False): - super().__init__(row_idx=row_idx, col_idx=col_idx, tracked=tracked) - self.vectors = vectors - - def fusion_algorithm(self, tiling: Tiling) -> FiniteFusion: - return FiniteFusion( - tiling, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked - ) - - def formal_step(self) -> str: - fusing = "rows" if self.row_idx is not None else "columns" - idx = self.row_idx if self.row_idx is not None else self.col_idx - return f"finite fuse {fusing} {idx} and {idx+1} with vectors {self.vectors}" - - def backward_map( - self, - comb_class: Tiling, - objs: Tuple[Optional[GriddedPerm], ...], - children: Optional[Tuple[Tiling, ...]] = None, - left_points: Optional[int] = None, - ) -> Iterator[GriddedPerm]: - """ - The backward direction of the underlying bijection used for object - generation and sampling. - """ - raise NotImplementedError - - def is_positive_or_empty_fusion(self, tiling: Tiling) -> bool: - raise NotImplementedError - - def constructor( - self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None - ) -> FusionConstructor: - raise NotImplementedError - - def reverse_constructor( # pylint: disable=no-self-use - self, - idx: int, - comb_class: Tiling, - children: Optional[Tuple[Tiling, ...]] = None, - ) -> Constructor: - raise NotImplementedError - - -class FiniteFusionFactory(StrategyFactory[Tiling]): - def __init__(self, tracked: bool = False, isolation_level: Optional[str] = None): - self.tracked = tracked - self.isolation_level = isolation_level - - def __call__(self, comb_class: Tiling) -> Iterator[Rule]: - if comb_class.requirements: - return - cols, rows = comb_class.dimensions - for row_idx in range(rows - 1): - algo = FiniteFusion( - comb_class, - row_idx=row_idx, - tracked=self.tracked, - isolation_level=self.isolation_level, - ) - if algo.fusable(): - fused_tiling = algo.fused_tiling() - yield FiniteFusionStrategy( - set(algo.get_valid_gap_vectors()), - row_idx=row_idx, - tracked=self.tracked, - )(comb_class, (fused_tiling,)) - for col_idx in range(cols - 1): - algo = FiniteFusion( - comb_class, - col_idx=col_idx, - tracked=self.tracked, - isolation_level=self.isolation_level, - ) - if algo.fusable(): - fused_tiling = algo.fused_tiling() - yield FiniteFusionStrategy( - set(algo.get_valid_gap_vectors()), - col_idx=col_idx, - tracked=self.tracked, - )(comb_class, (fused_tiling,)) - - def __str__(self) -> str: - return f"{'tracked ' if self.tracked else ''}finite fusion" - - def __repr__(self) -> str: - return ( - self.__class__.__name__ - + f"(tracked={self.tracked}, isolation_level={self.isolation_level})" - ) - - def to_jsonable(self) -> dict: - d: dict = super().to_jsonable() - d["tracked"] = self.tracked - d["isolation_level"] = self.isolation_level - return d - - @classmethod - def from_dict(cls, d: dict) -> "FiniteFusionStrategy": - return cls(**d) diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py new file mode 100644 index 00000000..679fc6e2 --- /dev/null +++ b/tilings/strategies/gap_vectors.py @@ -0,0 +1,141 @@ +from typing import Iterator, Optional, Tuple + +from comb_spec_searcher import Constructor, StrategyFactory +from comb_spec_searcher.exception import StrategyDoesNotApply +from comb_spec_searcher.strategies import Rule, Strategy +from tilings import GriddedPerm, Tiling +from tilings.algorithms.fusion import FiniteFusion + + +class RemoveGapVectorStrategy(Strategy[Tiling, GriddedPerm]): + """ + A strategy that restricts the set of gap vector for which you have permutation on + the tiling. + + The ouptut of the strategy is expected to be fusable with regular fusion. + """ + + def __init__(self, row_idx=None, col_idx=None, tracked: bool = False): + super().__init__(possibly_empty=False) + self.row_idx = row_idx + self.col_idx = col_idx + self.tracked = tracked + + def to_jsonable(self) -> dict: + raise NotImplementedError + + @classmethod + def from_dict(cls, d: dict) -> "RemoveGapVectorStrategy": + raise NotImplementedError + + def __call__( + self, + comb_class: Tiling, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Rule: + if children is None: + children = self.decomposition_function(comb_class) + if children is None: + raise StrategyDoesNotApply("Strategy does not apply") + return Rule(self, comb_class, children=children) + + 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,) + + def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]: + algo = self.fusion_algorithm(comb_class) + if algo.fusable() and algo.get_valid_gap_vectors(): + obs, req = algo.unfused_fused_obs_reqs() + ass = comb_class.assumptions + return (Tiling(obs, req, ass),) + raise StrategyDoesNotApply("Can't remove gap vectors") + + def fusion_algorithm(self, tiling: Tiling) -> FiniteFusion: + return FiniteFusion( + tiling, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked + ) + + def formal_step(self) -> str: + orientation = "rows" if self.row_idx is not None else "columns" + idx = self.row_idx if self.row_idx is not None else self.col_idx + return f"Removing gap vectors on {orientation} {idx} and {idx+1}" + + def forward_map( + self, + comb_class: Tiling, + obj: GriddedPerm, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Tuple[Optional[GriddedPerm], ...]: + raise NotImplementedError + + def backward_map( + self, + comb_class: Tiling, + objs: Tuple[Optional[GriddedPerm], ...], + children: Optional[Tuple[Tiling, ...]] = None, + left_points: Optional[int] = None, + ) -> Iterator[GriddedPerm]: + """ + The backward direction of the underlying bijection used for object + generation and sampling. + """ + raise NotImplementedError + + def constructor( + self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None + ): + raise NotImplementedError + + def reverse_constructor( # pylint: disable=no-self-use + self, + idx: int, + comb_class: Tiling, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Constructor: + raise NotImplementedError + + +class RemoveGapVectorFactory(StrategyFactory[Tiling]): + def __init__(self, tracked: bool = False): + self.tracked = tracked + + def __call__(self, comb_class: Tiling) -> Iterator[RemoveGapVectorStrategy]: + if comb_class.requirements: + return + cols, rows = comb_class.dimensions + for row_idx in range(rows - 1): + yield RemoveGapVectorStrategy( + row_idx=row_idx, + tracked=self.tracked, + ) + for col_idx in range(cols - 1): + yield RemoveGapVectorStrategy( + col_idx=col_idx, + tracked=self.tracked, + ) + + def __str__(self) -> str: + return f"{'tracked ' if self.tracked else ''} removed gap vectors" + + def __repr__(self) -> str: + return self.__class__.__name__ + f"(tracked={self.tracked})" + + def to_jsonable(self) -> dict: + d: dict = super().to_jsonable() + d["tracked"] = self.tracked + return d + + @classmethod + def from_dict(cls, d: dict) -> "RemoveGapVectorFactory": + return cls(**d) From b2d7e26baeea0706bdbc9d5ef3ae1d792659cc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 11:17:57 +0000 Subject: [PATCH 10/19] faster finite fusion algo --- tilings/algorithms/fusion.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 0fcf76be..3cca9fe5 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -694,6 +694,17 @@ def is_in_fuse_region(self, gp: GriddedPerm) -> bool: return all(x in (self._col_idx, self._col_idx + 1) for x, _ in gp.pos) return all(y in (self._row_idx, self._row_idx + 1) for _, y in gp.pos) + def fuses_to_all_perm_cell(self, fused_obs: Set[GriddedPerm]) -> bool: + if self._fuse_row: + return not any( + ob.is_single_cell() and any(ob.get_points_row(self._row_idx)) + for ob in fused_obs + ) + return not any( + ob.is_single_cell() and any(ob.get_points_col(self._col_idx)) + for ob in fused_obs + ) + def fused_tiling(self) -> "Tiling": if self._fused_tiling is None: extra_obs = self.extra_obs() @@ -702,10 +713,7 @@ def fused_tiling(self) -> "Tiling": for ob in self._tiling.obstructions if ob not in extra_obs ) - if any( - ob.is_single_cell() and any(ob.get_points_row(self._row_idx)) - for ob in fused_obs - ): + if not self.fuses_to_all_perm_cell(fused_obs): fused_reqs = set( frozenset(self.fuse_gridded_perm(gp) for gp in req) for req in self._tiling.requirements @@ -744,10 +752,15 @@ def unfused_fused_obs_reqs( def fusable(self) -> bool: obs, reqs = self.unfused_fused_obs_reqs() + reduced_obs = tuple( + o1 for o1 in obs if not any(o2 in o1 for o2 in self._tiling.obstructions) + ) return self._tiling == self._tiling.__class__( - self._tiling.obstructions + obs, + self._tiling.obstructions + reduced_obs, self._tiling.requirements + reqs, self._tiling.assumptions, + already_minimized_obs=True, + remove_empty_rows_and_cols=False, ) def extra_obs( @@ -806,10 +819,8 @@ def get_sk_from_gap_vector(self, vector: Tuple[int, int]) -> Iterator[GriddedPer yield GriddedPerm(gp.patt, new_pos) def is_valid_gap_vector(self, vector: Tuple[int, int]) -> bool: - return ( - self._tiling.add_obstructions(self.get_sk_from_gap_vector(vector)) - == self._tiling - ) + sk = self.get_sk_from_gap_vector(vector) + return all(gp.contains(*self._tiling.obstructions) for gp in sk) def get_valid_gap_vectors(self) -> Iterator[Tuple[int, int]]: gap_vectors = { From 565ebf6d49b7274dfef291983a3feb334d910d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 14:41:17 +0000 Subject: [PATCH 11/19] relaxing strategy and tracked remove --- tilings/algorithms/fusion.py | 29 +++++ tilings/strategies/gap_vectors.py | 205 +++++++++++++++++++++++++++--- 2 files changed, 216 insertions(+), 18 deletions(-) diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index 3cca9fe5..1a75436b 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -829,3 +829,32 @@ def get_valid_gap_vectors(self) -> Iterator[Tuple[int, int]]: if self.is_in_fuse_region(ob) } return filter(self.is_valid_gap_vector, gap_vectors) + + def assumption_for_gap_vector( + self, gv: Tuple[int, int] + ) -> Iterator[TrackingAssumption]: + """ + Returns the assumption that tracks the each row / column affected by the gap + vector. + """ + if self._fuse_row: + if gv[0] != 0: + yield TrackingAssumption.from_cells( + self._tiling.cells_in_row(self._row_idx) + ) + if gv[1] != 0: + yield TrackingAssumption.from_cells( + self._tiling.cells_in_row(self._row_idx + 1) + ) + else: + if gv[0] != 0: + yield TrackingAssumption.from_cells( + self._tiling.cells_in_col(self._col_idx) + ) + if gv[1] != 0: + yield TrackingAssumption.from_cells( + self._tiling.cells_in_col(self._col_idx + 1) + ) + + def assumptions_for_valid_gap_vectors(self) -> Iterator[TrackingAssumption]: + pass diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py index 679fc6e2..d3cf3433 100644 --- a/tilings/strategies/gap_vectors.py +++ b/tilings/strategies/gap_vectors.py @@ -1,3 +1,4 @@ +import itertools from typing import Iterator, Optional, Tuple from comb_spec_searcher import Constructor, StrategyFactory @@ -5,6 +6,7 @@ from comb_spec_searcher.strategies import Rule, Strategy from tilings import GriddedPerm, Tiling from tilings.algorithms.fusion import FiniteFusion +from tilings.strategies.assumption_insertion import AddAssumptionsStrategy class RemoveGapVectorStrategy(Strategy[Tiling, GriddedPerm]): @@ -28,17 +30,6 @@ def to_jsonable(self) -> dict: def from_dict(cls, d: dict) -> "RemoveGapVectorStrategy": raise NotImplementedError - def __call__( - self, - comb_class: Tiling, - children: Optional[Tuple[Tiling, ...]] = None, - ) -> Rule: - if children is None: - children = self.decomposition_function(comb_class) - if children is None: - raise StrategyDoesNotApply("Strategy does not apply") - return Rule(self, comb_class, children=children) - def can_be_equivalent(self) -> bool: return False @@ -54,18 +45,22 @@ def shifts( return (0,) def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]: - algo = self.fusion_algorithm(comb_class) - if algo.fusable() and algo.get_valid_gap_vectors(): + algo = FiniteFusion( + comb_class, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked + ) + gvs = set(algo.get_valid_gap_vectors()) + needed_ass = set( + itertools.chain.from_iterable( + algo.assumption_for_gap_vector(gv) for gv in gvs + ) + ) + has_right_ass = all(ass in comb_class.assumptions for ass in needed_ass) + if has_right_ass and algo.fusable() and algo.get_valid_gap_vectors(): obs, req = algo.unfused_fused_obs_reqs() ass = comb_class.assumptions return (Tiling(obs, req, ass),) raise StrategyDoesNotApply("Can't remove gap vectors") - def fusion_algorithm(self, tiling: Tiling) -> FiniteFusion: - return FiniteFusion( - tiling, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked - ) - def formal_step(self) -> str: orientation = "rows" if self.row_idx is not None else "columns" idx = self.row_idx if self.row_idx is not None else self.col_idx @@ -139,3 +134,177 @@ def to_jsonable(self) -> dict: @classmethod def from_dict(cls, d: dict) -> "RemoveGapVectorFactory": return cls(**d) + + +class RelaxingGapStrategy(Strategy[Tiling, GriddedPerm]): + """ + A strategy that relaxes the set of gap vector for which you have permutation on + the tiling. + """ + + def __init__( + self, + child: Tiling, + gv: Tuple[int, int], + *, + row_idx=None, + col_idx=None, + tracked: bool = False, + ): + super().__init__(possibly_empty=False) + self.row_idx = row_idx + self.col_idx = col_idx + self.tracked = tracked + self.gv = gv + algo = FiniteFusion(child, row_idx=row_idx, col_idx=col_idx) + self.child = child + if tracked: + self.child.add_assumptions(algo.assumption_for_gap_vector(gv)) + self.parent = child.add_obstructions(algo.get_sk_from_gap_vector(gv)) + + def to_jsonable(self) -> dict: + raise NotImplementedError + + @classmethod + def from_dict(cls, d: dict) -> "RemoveGapVectorStrategy": + raise NotImplementedError + + 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,) + + def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]: + if comb_class != self.parent: + raise StrategyDoesNotApply("Can't remove gap vectors") + return (self.child,) + + def formal_step(self) -> str: + orientation = "rows" if self.row_idx is not None else "columns" + idx = self.row_idx if self.row_idx is not None else self.col_idx + return f"Relaxing gap vectors on {orientation} {idx} and {idx+1} from {self.gv}" + + def forward_map( + self, + comb_class: Tiling, + obj: GriddedPerm, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Tuple[Optional[GriddedPerm], ...]: + raise NotImplementedError + + def backward_map( + self, + comb_class: Tiling, + objs: Tuple[Optional[GriddedPerm], ...], + children: Optional[Tuple[Tiling, ...]] = None, + left_points: Optional[int] = None, + ) -> Iterator[GriddedPerm]: + """ + The backward direction of the underlying bijection used for object + generation and sampling. + """ + raise NotImplementedError + + def constructor( + self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None + ): + raise NotImplementedError + + def reverse_constructor( # pylint: disable=no-self-use + self, + idx: int, + comb_class: Tiling, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Constructor: + raise NotImplementedError + + +class RelaxingGapFactory(StrategyFactory[Tiling]): + def __init__(self, tracked: bool = False): + self.tracked = tracked + + def __call__(self, comb_class: Tiling) -> Iterator[Rule]: + if comb_class.requirements: + return + cols, rows = comb_class.dimensions + for row_idx in range(rows - 1): + algo = FiniteFusion(comb_class, row_idx=row_idx) + gap_vectors = algo.get_valid_gap_vectors() + smaller_gap_vectors = set( + itertools.chain.from_iterable( + self.smaller_gap_vector(gv) for gv in gap_vectors + ) + ) + for gv in smaller_gap_vectors: + sk = algo.get_sk_from_gap_vector(gv) + parent = comb_class.add_obstructions(sk) + strategy = RelaxingGapStrategy( + comb_class, gv, row_idx=row_idx, tracked=self.tracked + ) + if self.tracked: + new_ass = [ + ass + for ass in algo.assumption_for_gap_vector(gv) + if ass not in comb_class.assumptions + ] + if new_ass: + yield AddAssumptionsStrategy(new_ass)(comb_class) + yield strategy(parent) + for col_idx in range(cols - 1): + algo = FiniteFusion(comb_class, col_idx=col_idx) + gap_vectors = algo.get_valid_gap_vectors() + smaller_gap_vectors = set( + itertools.chain.from_iterable( + self.smaller_gap_vector(gv) for gv in gap_vectors + ) + ) + for gv in smaller_gap_vectors: + sk = algo.get_sk_from_gap_vector(gv) + parent = comb_class.add_obstructions(sk) + strategy = RelaxingGapStrategy( + comb_class, gv, col_idx=col_idx, tracked=self.tracked + ) + if self.tracked: + new_ass = [ + ass + for ass in algo.assumption_for_gap_vector(gv) + if ass not in comb_class.assumptions + ] + if new_ass: + yield AddAssumptionsStrategy(new_ass)(comb_class) + yield strategy(parent) + + def smaller_gap_vector(self, gv: Tuple[int, int]) -> Iterator[Tuple[int, int]]: + if gv[0] == 0: + left = [0] + else: + left = list(range(1, gv[0] + 1)) + if gv[1] == 0: + right = [0] + else: + right = list(range(1, gv[1] + 1)) + return filter(gv.__ne__, itertools.product(left, right)) + + def __str__(self) -> str: + return "relaxing gap vectos" + + def __repr__(self) -> str: + return self.__class__.__name__ + f"(tracked={self.tracked})" + + def to_jsonable(self) -> dict: + d: dict = super().to_jsonable() + d["tracked"] = self.tracked + return d + + @classmethod + def from_dict(cls, d: dict) -> "RelaxingGapFactory": + return cls(**d) From beeca51f5db7bd571343e43772101842ce82fcc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 15:35:14 +0000 Subject: [PATCH 12/19] allow more smaller gap vectors --- tilings/strategies/gap_vectors.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py index d3cf3433..8982dd3f 100644 --- a/tilings/strategies/gap_vectors.py +++ b/tilings/strategies/gap_vectors.py @@ -284,15 +284,9 @@ def __call__(self, comb_class: Tiling) -> Iterator[Rule]: yield strategy(parent) def smaller_gap_vector(self, gv: Tuple[int, int]) -> Iterator[Tuple[int, int]]: - if gv[0] == 0: - left = [0] - else: - left = list(range(1, gv[0] + 1)) - if gv[1] == 0: - right = [0] - else: - right = list(range(1, gv[1] + 1)) - return filter(gv.__ne__, itertools.product(left, right)) + left = range(0, gv[0] + 1) + right = range(0, gv[1] + 1) + return filter((0, 0).__ne__, filter(gv.__ne__, itertools.product(left, right))) def __str__(self) -> str: return "relaxing gap vectos" From eeb3b9216af1c1f9e4424f51bc65568275cfe249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 15:47:44 +0000 Subject: [PATCH 13/19] rename strategy and put them in init --- tilings/strategies/__init__.py | 4 ++++ tilings/strategies/gap_vectors.py | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tilings/strategies/__init__.py b/tilings/strategies/__init__.py index 9e59df4f..92af1b72 100644 --- a/tilings/strategies/__init__.py +++ b/tilings/strategies/__init__.py @@ -11,6 +11,7 @@ ) from .factor import FactorFactory from .fusion import ComponentFusionFactory, FusionFactory +from .gap_vectors import RelaxGapVectorFactory, RemoveGapVectorFactory from .monotone_sliding import MonotoneSlidingFactory from .obstruction_inferral import ( EmptyCellInferralFactory, @@ -104,6 +105,9 @@ # Fusion "ComponentFusionFactory", "FusionFactory", + # Gap vector + "RelaxGapVectorFactory", + "RemoveGapVectorFactory", # Inferral "EmptyCellInferralFactory", "ObstructionInferralFactory", diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py index 8982dd3f..6f61e35a 100644 --- a/tilings/strategies/gap_vectors.py +++ b/tilings/strategies/gap_vectors.py @@ -136,7 +136,7 @@ def from_dict(cls, d: dict) -> "RemoveGapVectorFactory": return cls(**d) -class RelaxingGapStrategy(Strategy[Tiling, GriddedPerm]): +class RelaxGapVectorStrategy(Strategy[Tiling, GriddedPerm]): """ A strategy that relaxes the set of gap vector for which you have permutation on the tiling. @@ -228,7 +228,7 @@ def reverse_constructor( # pylint: disable=no-self-use raise NotImplementedError -class RelaxingGapFactory(StrategyFactory[Tiling]): +class RelaxGapVectorFactory(StrategyFactory[Tiling]): def __init__(self, tracked: bool = False): self.tracked = tracked @@ -247,7 +247,7 @@ def __call__(self, comb_class: Tiling) -> Iterator[Rule]: for gv in smaller_gap_vectors: sk = algo.get_sk_from_gap_vector(gv) parent = comb_class.add_obstructions(sk) - strategy = RelaxingGapStrategy( + strategy = RelaxGapVectorStrategy( comb_class, gv, row_idx=row_idx, tracked=self.tracked ) if self.tracked: @@ -270,7 +270,7 @@ def __call__(self, comb_class: Tiling) -> Iterator[Rule]: for gv in smaller_gap_vectors: sk = algo.get_sk_from_gap_vector(gv) parent = comb_class.add_obstructions(sk) - strategy = RelaxingGapStrategy( + strategy = RelaxGapVectorStrategy( comb_class, gv, col_idx=col_idx, tracked=self.tracked ) if self.tracked: @@ -300,5 +300,5 @@ def to_jsonable(self) -> dict: return d @classmethod - def from_dict(cls, d: dict) -> "RelaxingGapFactory": + def from_dict(cls, d: dict) -> "RelaxGapVectorFactory": return cls(**d) From a928fe85326f3a7fb3786ddb2bf813b030515a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 16:07:34 +0000 Subject: [PATCH 14/19] enumeration scheme pack --- tilings/strategy_pack.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tilings/strategy_pack.py b/tilings/strategy_pack.py index 4809a0b2..df4deb7b 100644 --- a/tilings/strategy_pack.py +++ b/tilings/strategy_pack.py @@ -859,3 +859,38 @@ def cell_insertions(cls, length: int): expansion_strats=[[strat.CellInsertionFactory(maxreqlen=length)]], name=f"length_{length}_cell_insertions", ) + + @classmethod + def enumeration_scheme(cls, direction: int) -> "TileScopePack": + """ + A pack that we think should mimick Vatter's enumeration scheme. + """ + if direction not in DIRS: + raise ValueError(f"Must be direction in {DIRS}.") + place_row = direction in (DIR_NORTH, DIR_SOUTH) + place_col = not place_row + direction_str = { + DIR_EAST: "right", + DIR_WEST: "left", + DIR_NORTH: "top", + DIR_SOUTH: "bottom", + }[direction] + name = f"{direction_str}_enumeration_scheme" + return TileScopePack( + initial_strats=[ + strat.FactorFactory(), + strat.RemoveGapVectorFactory(tracked=False), + strat.RelaxGapVectorFactory(tracked=False), + strat.FusionFactory(tracked=False), + ], + inferral_strats=[], + ver_strats=[strat.BasicVerificationStrategy()], + expansion_strats=[ + [ + strat.RowAndColumnPlacementFactory( + place_col=place_col, place_row=place_row, dirs=[direction] + ) + ] + ], + name=name, + ) From f533748d6c88c94e84e6b828a1ed124a3e8afa1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 16:27:27 +0000 Subject: [PATCH 15/19] fix untrack remove assumption --- tilings/strategies/gap_vectors.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py index 6f61e35a..d43ff214 100644 --- a/tilings/strategies/gap_vectors.py +++ b/tilings/strategies/gap_vectors.py @@ -49,13 +49,17 @@ def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]: comb_class, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked ) gvs = set(algo.get_valid_gap_vectors()) - needed_ass = set( - itertools.chain.from_iterable( - algo.assumption_for_gap_vector(gv) for gv in gvs + if self.tracked: + needed_ass = set( + itertools.chain.from_iterable( + algo.assumption_for_gap_vector(gv) for gv in gvs + ) ) - ) - has_right_ass = all(ass in comb_class.assumptions for ass in needed_ass) - if has_right_ass and algo.fusable() and algo.get_valid_gap_vectors(): + if not all(ass in comb_class.assumptions for ass in needed_ass): + raise StrategyDoesNotApply( + "Do not have right assumption to remove gap vectors" + ) + if algo.fusable() and algo.get_valid_gap_vectors(): obs, req = algo.unfused_fused_obs_reqs() ass = comb_class.assumptions return (Tiling(obs, req, ass),) From 0dbcac071013c5a79a7fcad60e539d68df45d5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 16:28:25 +0000 Subject: [PATCH 16/19] fix factories str --- tilings/strategies/gap_vectors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py index d43ff214..90225396 100644 --- a/tilings/strategies/gap_vectors.py +++ b/tilings/strategies/gap_vectors.py @@ -125,7 +125,7 @@ def __call__(self, comb_class: Tiling) -> Iterator[RemoveGapVectorStrategy]: ) def __str__(self) -> str: - return f"{'tracked ' if self.tracked else ''} removed gap vectors" + return f"{'tracked ' if self.tracked else ''}removed gap vectors" def __repr__(self) -> str: return self.__class__.__name__ + f"(tracked={self.tracked})" @@ -293,7 +293,7 @@ def smaller_gap_vector(self, gv: Tuple[int, int]) -> Iterator[Tuple[int, int]]: return filter((0, 0).__ne__, filter(gv.__ne__, itertools.product(left, right))) def __str__(self) -> str: - return "relaxing gap vectos" + return f"{'tracked ' if self.tracked else ''}relaxing gap vectors" def __repr__(self) -> str: return self.__class__.__name__ + f"(tracked={self.tracked})" From 6332cada6b111b805d249adc158cc581eb85ec14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Wed, 1 Jun 2022 16:29:46 +0000 Subject: [PATCH 17/19] json for strategies --- tilings/strategies/gap_vectors.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py index 90225396..e8394488 100644 --- a/tilings/strategies/gap_vectors.py +++ b/tilings/strategies/gap_vectors.py @@ -24,11 +24,19 @@ def __init__(self, row_idx=None, col_idx=None, tracked: bool = False): self.tracked = tracked def to_jsonable(self) -> dict: - raise NotImplementedError + d = super().to_jsonable() + d.pop("ignore_parent") + d.pop("inferrable") + d.pop("possibly_empty") + d.pop("workable") + d["row_idx"] = self.row_idx + d["col_idx"] = self.col_idx + d["tracked"] = self.tracked + return d @classmethod def from_dict(cls, d: dict) -> "RemoveGapVectorStrategy": - raise NotImplementedError + return cls(**d) def can_be_equivalent(self) -> bool: return False @@ -167,11 +175,22 @@ def __init__( self.parent = child.add_obstructions(algo.get_sk_from_gap_vector(gv)) def to_jsonable(self) -> dict: - raise NotImplementedError + d = super().to_jsonable() + d.pop("ignore_parent") + d.pop("inferrable") + d.pop("possibly_empty") + d.pop("workable") + d["child"] = self.child.to_jsonable() + d["gv"] = self.gv + d["row_idx"] = self.row_idx + d["col_idx"] = self.col_idx + d["tracked"] = self.tracked + return d @classmethod - def from_dict(cls, d: dict) -> "RemoveGapVectorStrategy": - raise NotImplementedError + def from_dict(cls, d: dict) -> "RelaxGapVectorStrategy": + child = Tiling.from_dict(d.pop("child")) + return cls(child, **d) def can_be_equivalent(self) -> bool: return False From 4e5e58511ed4842a4dec59bba45c1ef6a93782ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Fri, 3 Jun 2022 16:20:49 +0000 Subject: [PATCH 18/19] row_col_map does preimage of tiling --- tilings/algorithms/map.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tilings/algorithms/map.py b/tilings/algorithms/map.py index 976068ed..8bff8454 100644 --- a/tilings/algorithms/map.py +++ b/tilings/algorithms/map.py @@ -4,7 +4,7 @@ from tilings.exception import InvalidOperationError if TYPE_CHECKING: - from tilings.assumptions import TrackingAssumption + from tilings.assumptions import Tiling, TrackingAssumption from tilings.griddedperm import GriddedPerm Cell = Tuple[int, int] @@ -192,6 +192,23 @@ def preimage_gps(self, gps: Iterable["GriddedPerm"]) -> Iterator["GriddedPerm"]: for gp in gps: yield from self.preimage_gp(gp) + def preimage_obstruction_and_requirements( + self, tiling: "Tiling" + ) -> Tuple[List["GriddedPerm"], List[List["GriddedPerm"]]]: + if tiling.assumptions: + raise NotImplementedError("Not implemented for tilings with assumptions") + obs = itertools.chain.from_iterable( + self.preimage_gp(ob) for ob in tiling.obstructions + ) + reqs = ( + itertools.chain.from_iterable(self.preimage_gp(req) for req in req_list) + for req_list in tiling.requirements + ) + return list(obs), list(list(r) for r in reqs) + + def preimage_tiling(self, tiling: "Tiling") -> "Tiling": + return tiling.__class__(*self.preimage_obstruction_and_requirements(tiling)) + # Other method def max_row(self) -> int: """Return the biggest row index in the image.""" From 0674eb0b3e5e1b5415fa64f437ab660aa95aea8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20Nadeau?= Date: Fri, 3 Jun 2022 16:21:33 +0000 Subject: [PATCH 19/19] enumeration scheme recursion factory --- tilings/strategies/enumeration_scheme.py | 171 +++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 tilings/strategies/enumeration_scheme.py diff --git a/tilings/strategies/enumeration_scheme.py b/tilings/strategies/enumeration_scheme.py new file mode 100644 index 00000000..fc43e2c3 --- /dev/null +++ b/tilings/strategies/enumeration_scheme.py @@ -0,0 +1,171 @@ +import itertools +from typing import Iterator, Optional, Tuple + +from comb_spec_searcher.exception import StrategyDoesNotApply +from comb_spec_searcher.strategies import Constructor, Rule, Strategy, StrategyFactory +from permuta import Perm +from tilings import GriddedPerm, Tiling +from tilings.algorithms.map import RowColMap +from tilings.strategies.fusion import FusionStrategy + + +def all_gap_vectors(length: int, max_norm: int) -> Iterator[Tuple[int, ...]]: + if length == 0: + yield () + else: + for n in range(max_norm + 1): + for v in all_gap_vectors(length - 1, max_norm - n): + yield (n,) + v + + +def is_good_gap_vector(gv: Tuple[int, ...]): + # Change to 2 for ignoring vector that makes cell empty + return sum(gv) > 1 + + +def good_gap_vectors(length: int, max_norm: int) -> Iterator[Tuple[int, ...]]: + return filter(is_good_gap_vector, all_gap_vectors(length, max_norm)) + + +class ForbidGapVectorStrategy(Strategy[Tiling, GriddedPerm]): + """ + A strategy that restricts the set of gap vector for which you have permutation on + the tiling. + + The ouptut of the strategy is expected to be fusable with regular fusion. + """ + + def __init__(self, gap_vectors: Tuple[Tuple[int, ...], ...], child: Tiling): + super().__init__(workable=False) + self.gap_vectors = gap_vectors + self.child = child + + def to_jsonable(self) -> dict: + raise NotImplementedError + + @classmethod + def from_dict(cls, d: dict) -> "ForbidGapVectorStrategy": + raise NotImplementedError + + 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,) + + def expected_parent(self) -> Tiling: + """ + Return the tiling that is obtained by forbidding the gap vectors on the child + tiling. + """ + new_obs = itertools.chain.from_iterable(map(self.sk_from_gap, self.gap_vectors)) + return self.child.add_obstructions(new_obs) + + def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]: + if self.expected_parent() == comb_class: + return (self.child,) + raise StrategyDoesNotApply("Dimension don't match with gap vectors.") + + def formal_step(self) -> str: + return f"Forbid gap vectors {', '.join(map(str, self.gap_vectors))}" + + def forward_map( + self, + comb_class: Tiling, + obj: GriddedPerm, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Tuple[Optional[GriddedPerm], ...]: + raise NotImplementedError + + def backward_map( + self, + comb_class: Tiling, + objs: Tuple[Optional[GriddedPerm], ...], + children: Optional[Tuple[Tiling, ...]] = None, + left_points: Optional[int] = None, + ) -> Iterator[GriddedPerm]: + """ + The backward direction of the underlying bijection used for object + generation and sampling. + """ + raise NotImplementedError + + def constructor( + self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None + ): + raise NotImplementedError + + def reverse_constructor( # pylint: disable=no-self-use + self, + idx: int, + comb_class: Tiling, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Constructor: + raise NotImplementedError + + def sk_from_gap(self, gap_vector: Tuple[int, ...]) -> Iterator[GriddedPerm]: + col_pos_seq = tuple( + itertools.chain.from_iterable( + itertools.repeat(n, num) for n, num in enumerate(gap_vector) + ) + ) + for perm in Perm.of_length(len(col_pos_seq)): + yield GriddedPerm(perm, ((c, 0) for c in col_pos_seq)) + + +class EnumerationSchemeRecursionFactory(StrategyFactory): + def __init__(self, max_norm: int, max_num_vec: int) -> None: + self.max_norm = max_norm + self.max_num_vec = max_num_vec + + def __call__(self, comb_class: Tiling) -> Iterator[Rule[Tiling, GriddedPerm]]: + unfused_tilings = [] + for tiling, strat in self.fusion_strategy(comb_class): + unfused_tilings.append(tiling) + yield strat(tiling) + n_row, n_col = comb_class.dimensions + gap_vectors = list( + good_gap_vectors(comb_class.dimensions[0] + 1, self.max_norm) + ) + print(gap_vectors) + for n in range(1, self.max_num_vec + 1): + for vec_set in itertools.combinations(gap_vectors, n): + for t in unfused_tilings: + forbid_strat = ForbidGapVectorStrategy(vec_set, t) + parent = forbid_strat.expected_parent() + yield forbid_strat(parent, (t,)) + + def fusion_strategy( + self, comb_class: Tiling + ) -> Iterator[Tuple[Tiling, FusionStrategy]]: + """ + Iterator of pair tiling, fusion strategy for all the fusion rule we want to + create. + """ + num_col, num_row = comb_class.dimensions + assert num_row == 1 + for col in range(num_col): + fuse_col_map = {i: i for i in range(num_col)} + fuse_col_map.update({i: i - 1 for i in range(col + 1, num_col + 1)}) + fuse_map = RowColMap( + row_map={0: 0}, col_map=fuse_col_map, is_identity=False + ) + fusion_strat = FusionStrategy(col_idx=col) + yield fuse_map.preimage_tiling(comb_class), fusion_strat + + def __repr__(self) -> str: + return "Repr" + + def __str__(self) -> str: + return "enumeration recursion" + + def from_dict(self, d): + raise NotImplementedError