diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index f5092357..a637b650 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -11,9 +11,12 @@ Iterator, List, Optional, + Set, Tuple, ) +from permuta import Perm +from tilings.algorithms.map import RowColMap from tilings.assumptions import ( ComponentAssumption, SkewComponentAssumption, @@ -680,3 +683,195 @@ def __str__(self) -> str: s = "ComponentFusion Algorithm for:\n" s += str(self._tiling) return s + + +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: + 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() + fused_obs = set( + self.fuse_gridded_perm(ob) + for ob in self._tiling.obstructions + if ob not in extra_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 + ) + 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( + 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: + 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 + reduced_obs, + self._tiling.requirements + reqs, + self._tiling.assumptions, + already_minimized_obs=True, + remove_empty_rows_and_cols=False, + ) + + def extra_obs( + self, + ) -> Set[GriddedPerm]: + valid_gap_vectors = set(self.get_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 ( + 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]) -> 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_valid_gap_vector(self, vector: Tuple[int, int]) -> bool: + 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 = { + 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) + + 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/algorithms/map.py b/tilings/algorithms/map.py index feaf2d21..8bff8454 100644 --- a/tilings/algorithms/map.py +++ b/tilings/algorithms/map.py @@ -1,9 +1,10 @@ -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 if TYPE_CHECKING: - from tilings.assumptions import TrackingAssumption + from tilings.assumptions import Tiling, TrackingAssumption from tilings.griddedperm import GriddedPerm Cell = Tuple[int, int] @@ -117,6 +118,98 @@ 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) + + 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.""" return max(self._row_map.values()) 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/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 diff --git a/tilings/strategies/gap_vectors.py b/tilings/strategies/gap_vectors.py new file mode 100644 index 00000000..e8394488 --- /dev/null +++ b/tilings/strategies/gap_vectors.py @@ -0,0 +1,327 @@ +import itertools +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 +from tilings.strategies.assumption_insertion import AddAssumptionsStrategy + + +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: + 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": + return cls(**d) + + 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 = FiniteFusion( + comb_class, row_idx=self.row_idx, col_idx=self.col_idx, tracked=self.tracked + ) + gvs = set(algo.get_valid_gap_vectors()) + if self.tracked: + needed_ass = set( + itertools.chain.from_iterable( + algo.assumption_for_gap_vector(gv) for gv in gvs + ) + ) + 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),) + raise StrategyDoesNotApply("Can't remove gap vectors") + + 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) + + +class RelaxGapVectorStrategy(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: + 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) -> "RelaxGapVectorStrategy": + child = Tiling.from_dict(d.pop("child")) + return cls(child, **d) + + 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 RelaxGapVectorFactory(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 = RelaxGapVectorStrategy( + 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 = RelaxGapVectorStrategy( + 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]]: + 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 f"{'tracked ' if self.tracked else ''}relaxing 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) -> "RelaxGapVectorFactory": + return cls(**d) diff --git a/tilings/strategy_pack.py b/tilings/strategy_pack.py index 3ab43d72..30711f58 100644 --- a/tilings/strategy_pack.py +++ b/tilings/strategy_pack.py @@ -865,3 +865,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, + )