diff --git a/tilings/algorithms/fusion.py b/tilings/algorithms/fusion.py index f2553074..26098a53 100644 --- a/tilings/algorithms/fusion.py +++ b/tilings/algorithms/fusion.py @@ -423,6 +423,13 @@ def predicate_fusable(self) -> bool: elif isinstance(ass, OppositeParityAssumption): opposite.add(next(iter(ass.cells))) left = self.left_fuse_region() + if len(left) > 1: + # TODO: can't fuse things like + # O | O + # - + - + # O | O + # need entire rows/columns to be tracked as odd. + return False for (x, y) in left: xn, yn = x, y if self._fuse_row: @@ -436,7 +443,7 @@ def predicate_fusable(self) -> bool: return False return True - def fused_tiling(self) -> "Tiling": + def fused_tiling(self, add_odd_req: bool = True) -> "Tiling": """ Return the fused tiling. """ @@ -450,10 +457,14 @@ def fused_tiling(self) -> "Tiling": assumptions.extend(self.fused_predicates()) if self._tracked: assumptions.append(self.new_assumption()) - requirements = list(list(fc) for fc in self.requirements_fuse_counters) + requirements: List[Iterable[GriddedPerm]] = list( + list(fc) for fc in self.requirements_fuse_counters + ) if self._positive_left or self._positive_right: new_positive_requirement = self.new_positive_requirement() requirements = requirements + [new_positive_requirement] + if add_odd_req and self.is_odd_next_to_odd_fusion(): + requirements += [map(GriddedPerm.point_perm, self.left_fuse_region())] self._fused_tiling = self._tiling.__class__( obstructions=self.obstruction_fuse_counter.keys(), requirements=requirements, @@ -463,6 +474,17 @@ def fused_tiling(self) -> "Tiling": ) return self._fused_tiling + def clear_fused_tiling(self) -> None: + self._fused_tiling = None + + def is_odd_next_to_odd_fusion(self) -> bool: + return ( + OddCountAssumption.from_cells(self.left_fuse_region()) + in self._tiling.assumptions + and OddCountAssumption.from_cells(self.right_fuse_region()) + in self._tiling.assumptions + ) + def fuse_assumption(self, assumption: AssumptionClass) -> AssumptionClass: return assumption.__class__(self.fuse_gridded_perm(gp) for gp in assumption.gps) diff --git a/tilings/algorithms/minimal_gridded_perms.py b/tilings/algorithms/minimal_gridded_perms.py index 8e3dc839..985ff5a6 100644 --- a/tilings/algorithms/minimal_gridded_perms.py +++ b/tilings/algorithms/minimal_gridded_perms.py @@ -548,7 +548,7 @@ def odd_even_minimal_gridded_perms( self, odd_cells: Set[Cell], even_cells: Set[Cell] ) -> Set[GriddedPerm]: if odd_cells.intersection(even_cells): - return + return set() def cell_count(gp: GriddedPerm, cell: Cell) -> int: return sum(1 for _ in filter(cell.__eq__, gp.pos)) diff --git a/tilings/algorithms/obstruction_inferral.py b/tilings/algorithms/obstruction_inferral.py index cab6fd21..1fbb8f14 100644 --- a/tilings/algorithms/obstruction_inferral.py +++ b/tilings/algorithms/obstruction_inferral.py @@ -1,8 +1,10 @@ import abc from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple +from permuta import Perm from tilings import GriddedPerm from tilings.algorithms.gridded_perm_generation import GriddedPermsOnTiling +from tilings.assumptions import OddCountAssumption if TYPE_CHECKING: from tilings import Tiling @@ -39,13 +41,35 @@ def new_obs(self, yield_non_minimal: bool = False) -> List[GriddedPerm]: self._new_obs = [] return self._new_obs + extra_reqs = [] + assumptions = list(self._tiling.assumptions) + for ass in assumptions: + if isinstance(ass, OddCountAssumption) and len(ass.cells) == 1: + to_add = [GriddedPerm.single_cell(Perm((0,)), next(iter(ass.cells)))] + if to_add not in self._tiling.requirements: + extra_reqs.append(to_add) + + fake_tiling = self._tiling.__class__( + self._tiling.obstructions, + self._tiling.requirements + tuple(extra_reqs), + self._tiling.assumptions, + remove_empty_rows_and_cols=False, + derive_empty=False, + simplify=False, + sorted_input=False, + already_minimized_obs=True, + checked=True, + ) + max_len_of_perms_to_check = max(map(len, perms_to_check)) max_length = ( - self._tiling.maximum_length_of_minimum_gridded_perm() + fake_tiling.maximum_length_of_minimum_gridded_perm() + max_len_of_perms_to_check ) + GP = GriddedPermsOnTiling( - self._tiling, yield_non_minimal=yield_non_minimal + fake_tiling, + yield_non_minimal=yield_non_minimal, ).gridded_perms(max_length, place_at_most=max_len_of_perms_to_check) perms_left = set(perms_to_check) for gp in GP: @@ -122,8 +146,8 @@ def potential_new_obs(self) -> List[GriddedPerm]: """ Iterator over all possible obstruction of `self.obstruction_length`. """ - if not self._tiling.requirements: - return [] + # if not self._tiling.requirements: + # return [] no_req_tiling = self._tiling.__class__(self._tiling.obstructions) n = self._obs_len pot_obs = filter(self.not_required, no_req_tiling.gridded_perms(n)) diff --git a/tilings/strategies/__init__.py b/tilings/strategies/__init__.py index 9e59df4f..5422db36 100644 --- a/tilings/strategies/__init__.py +++ b/tilings/strategies/__init__.py @@ -48,7 +48,11 @@ from .row_and_col_separation import RowColumnSeparationStrategy from .sliding import SlidingFactory from .symmetry import SymmetriesFactory -from .unfusion import UnfusionColumnStrategy, UnfusionFactory, UnfusionRowStrategy +from .unfusion import ( + UnfusionColumnStrategy, + UnfusionRowColumnFactory, + UnfusionRowStrategy, +) from .verification import ( BasicVerificationStrategy, ComponentVerificationStrategy, @@ -93,7 +97,7 @@ "RequirementPointingFactory", "UnfusionColumnStrategy", "UnfusionRowStrategy", - "UnfusionFactory", + "UnfusionRowColumnFactory", # Equivalence "MonotoneSlidingFactory", "PatternPlacementFactory", diff --git a/tilings/strategies/fusion/component.py b/tilings/strategies/fusion/component.py index afb76fe3..3d480ba3 100644 --- a/tilings/strategies/fusion/component.py +++ b/tilings/strategies/fusion/component.py @@ -5,7 +5,6 @@ from tilings import GriddedPerm, Tiling from tilings.algorithms import ComponentFusion -from .constructor import FusionConstructor from .fusion import FusionStrategy @@ -39,7 +38,7 @@ def is_positive_or_empty_fusion(self, tiling: Tiling) -> bool: def constructor( self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None - ) -> FusionConstructor: + ) -> Constructor: if self.tracked and self.is_positive_or_empty_fusion(comb_class): raise NotImplementedError( "Can't count positive or empty fusion. Try a cell insertion!" diff --git a/tilings/strategies/fusion/fusion.py b/tilings/strategies/fusion/fusion.py index a646f9d0..14b7b685 100644 --- a/tilings/strategies/fusion/fusion.py +++ b/tilings/strategies/fusion/fusion.py @@ -1,5 +1,5 @@ from collections import defaultdict -from itertools import chain, islice +from itertools import chain, islice, product from random import randint from typing import Callable, Dict, Iterator, List, Optional, Set, Tuple, cast @@ -9,7 +9,8 @@ from comb_spec_searcher.typing import Objects from tilings import GriddedPerm, Tiling from tilings.algorithms import Fusion -from tilings.assumptions import OddCountAssumption +from tilings.assumptions import EvenCountAssumption, OddCountAssumption +from tilings.strategies.requirement_insertion import RequirementInsertionStrategy from ..pointing import DivideByK from .constructor import FusionConstructor, ReverseFusionConstructor @@ -195,7 +196,7 @@ def shifts( def constructor( self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None - ) -> FusionConstructor: + ) -> Constructor: if not self.tracked: # constructor only enumerates when tracked. raise NotImplementedError("The fusion strategy was not tracked.") @@ -207,6 +208,15 @@ def constructor( assert children is None or children == (child,) min_left, min_right = algo.min_left_right_points() left, right = algo.left_fuse_region(), algo.right_fuse_region() + odd_left, odd_right = None, None + if OddCountAssumption.from_cells(left) in comb_class.assumptions: + odd_left = True + elif EvenCountAssumption.from_cells(left) in comb_class.assumptions: + odd_left = False + if OddCountAssumption.from_cells(right) in comb_class.assumptions: + odd_right = True + if EvenCountAssumption.from_cells(right) in comb_class.assumptions: + odd_right = False return FusionConstructor( comb_class, child, @@ -215,8 +225,8 @@ def constructor( *self.left_right_both_sided_parameters(comb_class), min_left, min_right, - odd_left=(OddCountAssumption.from_cells(left) in comb_class.assumptions), - odd_right=(OddCountAssumption.from_cells(right) in comb_class.assumptions), + odd_left=odd_left, + odd_right=odd_right, ) def reverse_constructor( # pylint: disable=no-self-use @@ -421,6 +431,10 @@ def __call__(self, comb_class: Tiling) -> Iterator[Rule]: yield FusionStrategy(row_idx=row_idx, tracked=self.tracked)( comb_class, (fused_tiling,) ) + # yield from self._all_unfusion_rules(True, row_idx, fused_tiling) + if algo.is_odd_next_to_odd_fusion(): + yield self._extra_cell_insertion_rule(algo) + for col_idx in range(cols - 1): algo = Fusion( comb_class, @@ -433,6 +447,47 @@ def __call__(self, comb_class: Tiling) -> Iterator[Rule]: yield FusionStrategy(col_idx=col_idx, tracked=self.tracked)( comb_class, (fused_tiling,) ) + # yield from self._all_unfusion_rules(False, col_idx, fused_tiling) + if algo.is_odd_next_to_odd_fusion(): + yield self._extra_cell_insertion_rule(algo) + + def _all_unfusion_rules( + self, row: bool, idx: int, tiling: Tiling + ) -> Iterator[Rule]: + # pylint: disable=import-outside-toplevel + from tilings.strategies.fusion.unfusion import UnfusionStrategy + + for left, right, both in product((True, False), (True, False), (True, False)): + if left or right or both: + if row: + yield UnfusionStrategy( + row_idx=idx, + tracked=self.tracked, + left=left, + right=right, + both=both, + )(tiling) + else: + yield UnfusionStrategy( + col_idx=idx, + tracked=self.tracked, + left=left, + right=right, + both=both, + )(tiling) + + @staticmethod + def _extra_cell_insertion_rule(algo: Fusion) -> Rule: + algo.clear_fused_tiling() + fused = algo.fused_tiling(add_odd_req=False) + algo.clear_fused_tiling() + cells = [ + fused.forward_map.map_cell(cell) + for cell in filter( + fused.forward_map.is_mappable_cell, algo.left_fuse_region() + ) + ] + return RequirementInsertionStrategy(map(GriddedPerm.point_perm, cells))(fused) def __str__(self) -> str: if self.tracked: diff --git a/tilings/strategies/fusion/unfusion.py b/tilings/strategies/fusion/unfusion.py new file mode 100644 index 00000000..b3b000d3 --- /dev/null +++ b/tilings/strategies/fusion/unfusion.py @@ -0,0 +1,252 @@ +from itertools import chain +from typing import FrozenSet, Iterator, Optional, Set, Tuple, Union + +from comb_spec_searcher.exception import StrategyDoesNotApply +from comb_spec_searcher.strategies.constructor.base import Constructor +from tilings import GriddedPerm, Tiling +from tilings.assumptions import Assumption, Cell, TrackingAssumption +from tilings.strategies.fusion.constructor import ReverseFusionConstructor +from tilings.strategies.fusion.fusion import FusionRule, FusionStrategy +from tilings.strategies.pointing import DivideByK + + +class UnfusionRule(FusionRule): + def _ensure_level_objects(self, n: int) -> None: + raise NotImplementedError + + def random_sample_object_of_size(self, n: int, **parameters: int) -> GriddedPerm: + raise NotImplementedError + + def _forward_order( + self, + obj: GriddedPerm, + image: Tuple[Optional[GriddedPerm], ...], + data: Optional[object] = None, + ) -> int: + raise NotImplementedError + + def _backward_order_item( + self, + idx: int, + objs: Tuple[Optional[GriddedPerm], ...], + data: Optional[object] = None, + ) -> GriddedPerm: + raise NotImplementedError + + +class UnfusionStrategy(FusionStrategy): + def __init__( + self, + row_idx=None, + col_idx=None, + tracked: bool = False, + left: bool = False, + right: bool = False, + both: bool = False, + ): + super().__init__(row_idx, col_idx, tracked) + self.left = left + self.right = right + self.both = both + assert left or right or both or not self.tracked + + def __call__( + self, + comb_class: Tiling, + children: Tuple[Tiling, ...] = None, + ) -> UnfusionRule: + if children is None: + children = self.decomposition_function(comb_class) + if children is None: + raise StrategyDoesNotApply("Strategy does not apply") + return UnfusionRule(self, comb_class, children=children) + + def decomposition_function(self, comb_class: Tiling) -> Tuple[Tiling]: + fused_region = self.fused_region(comb_class) + + def valid_fusion_assumptions(): + + if ( + TrackingAssumption.from_cells(fused_region) + not in comb_class.assumptions + ): + return False + for assumption in comb_class.tracking_assumptions: + intersected = fused_region.intersection(assumption.cells) + if intersected and intersected != fused_region: + # strategy does not apply + return False + return True + + if valid_fusion_assumptions(): + return (self.unfused_tiling(comb_class),) + + def unfused_tiling(self, tiling: Tiling) -> Tiling: + algo = self.fusion_algorithm(tiling) + obs = chain(*[algo.unfuse_gridded_perm(ob) for ob in tiling.obstructions]) + reqs = [ + [gp for req_gp in req_list for gp in algo.unfuse_gridded_perm(req_gp)] + for req_list in tiling.requirements + ] + ass = set( + ass.__class__( + [gp for ass_gp in ass.gps for gp in algo.unfuse_gridded_perm(ass_gp)] + ) + for ass in tiling.assumptions + ) + if self.tracked: + + def add_or_remove( + assumptions: Set[Assumption], assumption: TrackingAssumption, add: bool + ) -> Set[Assumption]: + if add: + assumptions.add(assumption) + else: + assumptions.discard(assumption) + + add_or_remove(ass, self.left_tracking_assumption(tiling), self.left) + add_or_remove(ass, self.right_tracking_assumption(tiling), self.right) + add_or_remove(ass, self.both_tracking_assumption(tiling), self.both) + return Tiling(obs, reqs, ass) + + def fused_region(self, tiling: Tiling) -> FrozenSet[Cell]: + if self.row_idx is not None: + return tiling.cells_in_row(self.row_idx) + return tiling.cells_in_col(self.col_idx) + + def left_unfused_region(self, tiling: Tiling) -> FrozenSet[Cell]: + return self.fused_region(tiling) + + def right_unfused_region(self, tiling: Tiling) -> FrozenSet[Cell]: + if self.row_idx is not None: + return frozenset((x, y + 1) for (x, y) in self.fused_region(tiling)) + return frozenset((x + 1, y) for (x, y) in self.fused_region(tiling)) + + def unfused_region(self, tiling: Tiling) -> FrozenSet[Cell]: + return self.left_unfused_region(tiling).union(self.right_unfused_region(tiling)) + + def left_tracking_assumption(self, tiling: Tiling) -> TrackingAssumption: + return TrackingAssumption.from_cells(self.left_unfused_region(tiling)) + + def right_tracking_assumption(self, tiling: Tiling) -> TrackingAssumption: + return TrackingAssumption.from_cells(self.right_unfused_region(tiling)) + + def both_tracking_assumption(self, tiling: Tiling) -> TrackingAssumption: + return TrackingAssumption.from_cells(self.unfused_region(tiling)) + + def is_reversible(self, comb_class: Tiling) -> bool: + return False + + def constructor( + self, comb_class: Tiling, children: Optional[Tuple[Tiling, ...]] = None + ) -> Union[DivideByK, ReverseFusionConstructor]: + if not self.tracked: + # constructor only enumerates when tracked. + raise NotImplementedError("The fusion strategy was not tracked.") + if children is None: + children = self.decomposition_function(comb_class) + assert children is not None + fused_tiling, unfused_tiling = comb_class, children[0] + # Need to recompute some info to count, so ignoring passed in children + algo = self.fusion_algorithm(unfused_tiling) + # if not algo.fusable(): + # raise StrategyDoesNotApply("Strategy does not apply") + if algo.min_left_right_points() != (0, 0): + raise NotImplementedError( + "Reverse positive fusion counting not implemented" + ) + ( + left_sided_params, + right_sided_params, + _, + ) = self.left_right_both_sided_parameters(unfused_tiling) + if not self.left and not self.right: + assert self.both + unfused_assumption = self.both_tracking_assumption(comb_class) + assert unfused_assumption in unfused_tiling.assumptions + return DivideByK( + fused_tiling, + (unfused_tiling,), + 1, + unfused_tiling.get_assumption_parameter(unfused_assumption), + self.extra_parameters(unfused_tiling, (fused_tiling,)), + ) + return ReverseFusionConstructor( + unfused_tiling, + comb_class, + self._fuse_parameter(fused_tiling), + self.extra_parameters(unfused_tiling, (fused_tiling,))[0], + tuple(left_sided_params), + tuple(right_sided_params), + ) + + def reverse_constructor( + self, + idx: int, + comb_class: Tiling, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Constructor: + raise NotImplementedError + + def _fuse_parameter(self, comb_class: Tiling) -> str: + return comb_class.get_assumption_parameter( + TrackingAssumption.from_cells(self.fused_region(comb_class)) + ) + + 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"unfuse {fusing} {idx}" + + def backward_map( + self, + comb_class: Tiling, + objs: Tuple[Optional[GriddedPerm], ...], + children: Optional[Tuple[Tiling, ...]] = None, + left_points: int = None, + ) -> Iterator[GriddedPerm]: + raise NotImplementedError + + def forward_map( + self, + comb_class: Tiling, + obj: GriddedPerm, + children: Optional[Tuple[Tiling, ...]] = None, + ) -> Tuple[Optional[GriddedPerm], ...]: + raise NotImplementedError + + +# if __name__ == "__main__": +# from tilings.assumptions import OddCountAssumption, OppositeParityAssumption +# from tilings.strategies import FusionFactory + +# t = Tiling( +# obstructions=[ +# GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (0, 0))), +# GriddedPerm((0, 1, 2), ((0, 0), (0, 0), (1, 0))), +# GriddedPerm((0, 1, 2), ((0, 0), (1, 0), (1, 0))), +# GriddedPerm((0, 1, 2), ((1, 0), (1, 0), (1, 0))), +# ], +# assumptions=[ +# TrackingAssumption.from_cells([(0, 0)]), +# OddCountAssumption.from_cells([(0, 0)]), +# OddCountAssumption.from_cells([(1, 0)]), +# OppositeParityAssumption.from_cells([(0, 0)]), +# OppositeParityAssumption.from_cells([(1, 0)]), +# ], +# ) + +# for rule in FusionFactory()(t): +# print(rule) +# print(t) +# for left in (True, False): +# for right in (True, False): +# for both in (True, False): +# if left or right or both: +# strat = UnfusionStrategy( +# 0, None, True, left=left, right=right, both=both +# ) +# rule = strat(t) +# for i in range(6): +# print(i, rule.sanity_check(i)) +# # rule.sanity_check(i) diff --git a/tilings/strategies/obstruction_inferral.py b/tilings/strategies/obstruction_inferral.py index 35020ce9..6a1faeb1 100644 --- a/tilings/strategies/obstruction_inferral.py +++ b/tilings/strategies/obstruction_inferral.py @@ -124,7 +124,11 @@ def new_obs(self, tiling: Tiling) -> Sequence[GriddedPerm]: def __call__(self, comb_class: Tiling) -> Iterator[ObstructionInferralStrategy]: gps = self.new_obs(comb_class) if gps: - yield ObstructionInferralStrategy(gps) + OIS = ObstructionInferralStrategy(gps) + # print("=" * 30) + # print(comb_class) + # print(OIS.decomposition_function(comb_class)[0]) + yield OIS def to_jsonable(self) -> dict: d: dict = super().to_jsonable() diff --git a/tilings/strategies/unfusion.py b/tilings/strategies/unfusion.py index a9ac38f3..b6ce95ad 100644 --- a/tilings/strategies/unfusion.py +++ b/tilings/strategies/unfusion.py @@ -301,7 +301,7 @@ def __init__( super().__init__(ignore_parent, inferrable, possibly_empty, workable, cols) -class UnfusionFactory(StrategyFactory[Tiling]): +class UnfusionRowColumnFactory(StrategyFactory[Tiling]): def __init__(self, max_width: int = 4, max_height: int = 4) -> None: self.max_height = max_height self.max_width = max_width @@ -321,5 +321,5 @@ def __repr__(self) -> str: return self.__class__.__name__ + "()" @classmethod - def from_dict(cls, d: dict) -> "UnfusionFactory": + def from_dict(cls, d: dict) -> "UnfusionRowColumnFactory": return cls() diff --git a/tilings/strategy_pack.py b/tilings/strategy_pack.py index 7bc21a9e..b28bab47 100644 --- a/tilings/strategy_pack.py +++ b/tilings/strategy_pack.py @@ -379,7 +379,7 @@ def kitchen_sinkify( # pylint: disable=R0912 strat.AssumptionPointingFactory(), strat.RequirementPointingFactory(), strat.PointingStrategy(), - strat.UnfusionFactory(), + strat.UnfusionRowColumnFactory(), strat.FusableRowAndColumnPlacementFactory(), ), ) diff --git a/tilings/tilescope.py b/tilings/tilescope.py index b04e8e3a..56aa8db7 100644 --- a/tilings/tilescope.py +++ b/tilings/tilescope.py @@ -434,8 +434,11 @@ def backward_map( ) -> Iterator[GriddedPerm]: if children is None: children = self.decomposition_function(comb_class) - gp = next((gp for gp in objs if gp is not None)) - yield gp + try: + gp = next((gp for gp in objs if gp is not None)) + yield gp + except StopIteration as e: + raise ValueError from e def forward_map( self, diff --git a/tilings/tiling.py b/tilings/tiling.py index bdc1b726..4836eea3 100644 --- a/tilings/tiling.py +++ b/tilings/tiling.py @@ -164,7 +164,7 @@ def __init__( else: self.set_empty() - self._check_init(checked) + self.check_init(checked) def _set_obstructions_requirements_and_assumptions( self, @@ -197,7 +197,7 @@ def _set_obstructions_requirements_and_assumptions( _assumptions.append(ass) self._assumptions = tuple(sorted(_assumptions)) - def _check_init(self, checked: bool): + def check_init(self, checked: bool): if DEBUG and not checked: redone = Tiling( self._obstructions, self._requirements, self._assumptions, checked=True @@ -873,9 +873,12 @@ def add_assumptions(self, assumptions: Iterable[Assumption]) -> "Tiling": remove_empty_rows_and_cols=remove_empty_rows_and_cols, derive_empty=derive_empty, simplify=simplify, + checked=True, ) if not simplify: tiling.clean_assumptions() + if DEBUG: + tiling.check_init(False) return tiling def remove_assumption(self, assumption: Assumption): @@ -1704,6 +1707,7 @@ def is_empty(self, experimental_bound: Optional[int] = None) -> bool: TODO: this method ignores predicates """ + # pylint: disable = arguments-differ if any(ob.is_empty() for ob in self.obstructions) or any( not ass.can_be_satisfied(self) for ass in self.predicate_assumptions ):